先發(fā)一張昨天去看我雷哥演唱會(huì)的皂片然后再說正文哈哈。

簡(jiǎn)介
由于工作原因,boss下達(dá)的任務(wù)就大概說了對(duì)圖片進(jìn)行壓縮尋找比較合理的方式,還舉了一個(gè)項(xiàng)目中的坑,就是系統(tǒng)原生的Bitmap.compress設(shè)置質(zhì)量參數(shù)為100生成圖片會(huì)變大的坑。所以我打算用一點(diǎn)時(shí)間研究研究Bitmap在內(nèi)存和外存中的情況。
首先需要對(duì)圖片進(jìn)行壓縮,大家都知道圖片是Android里面一個(gè)大坑,具體的問題有
- oom,一不留神就用OOM來(lái)沖沖喜,所以網(wǎng)上就有了很多解決oom問題的建議,但是由于網(wǎng)友的水平不一也導(dǎo)致建議參差不齊。(內(nèi)存)
- 圖片壓縮再加載失真嚴(yán)重,或者壓縮率不夠達(dá)不到項(xiàng)目要求的效果。(外存)
那我今天就要解決的就是通過今天查閱的資料和自己的判斷,還有實(shí)踐歸檔一下圖片在Android上的問題。并且給出自己解決圖片壓縮問題的解決方案和實(shí)際操作。
1. 為什么Android上的圖片就不如IOS上的?
我在全球男性交友中心找到了一個(gè)外國(guó)基友寫的一段情書:
gihubLink
There are so many comparations between Android phone and iPhone. We cannot make the conclusion about which one is better, but we all knows that the image quality of Android phone is much worse than iPhone. No matter you are using Facebook, Twitter or even Instagram, after taking the photo, adding a filter, then sharing to the social network, the images produced by Android phone are always coarse. Why?
Our team had been working on this issue in the last year. After very deep research, we found that this was a "TINY" mistake made by Google. Although tiny, but the influence was very huge (all Android Apps related to image), and lasted till today.
The problem is : libjpeg.
We all know that libjpeg is widely used open source JPEG library. Android also uses libjpeg to compress images. After digging into the source code of Android, we can find that instead of using libjpeg directly, Android is based on an open source image engine called Skia. The Skia is a wonderful engine maintained by Google himself, all image functions are implemented in it, and it is widely used by Google and other companies' products (e.g.: Chrome, Firefox, Android......). The Skia has a good encapsulation of libjpeg, you can easily develop image utilites base on this engine.
When using libjpeg to compress images, optimize_coding is a very important parameter. In libjpeg.doc, we can find following introductions about this parameter:
boolean optimize_coding
TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.
As the libjpeg.doc, we now know that because setting the optimize_coding to TRUE may cost a good deal of space and time, the default in libjpeg is FALSE.
Everything seems fine about the doc, and libjpeg is very stable. But many people ignored that this document was writen for more than 10 years. At that time, space and computing abilities are very limited. With today's modern computers or even mobile phones, this is not an issue. On the contrary, we should pay more attention to the image quality (retina screens) and image size (cloud services).
Google's engineers of skia project did not set this parameter, so the optimize_coding in Skia was remained to FALSE as the default value, and Skia concealed this setting, you could not change the setting outside of Skia. This became to a big problem, we had to endure worse image and bigger file size.
Our team had tested optimize_coding for many different images. If you want the same quality of image compressing, the file size are 5-10 times bigger when setting the optimze_coding to FALSE than to TRUE. The difference is quite significant.
We also compared the jpeg compressing between iOS and Android (they both concealed the optimize_coding parameter). With the same original images, if you want same quality level, you need 5-10 times file size on Android.
The result is clear, Apple does know the importance of optimize_coding and Huffman tables and Google does not. (Apple uses their own Huffman table algorithm, not like libjpeg or libjpeg-turbo. It seems that Apple has done more tuning works on image compressing.)
Finally, we decided not to use JPEG compress functions provided by Android, and we compiled our own native library based on libjpeg-turbo (libjpeg-turbo also has performance improvements). Now we can save 5-10 times of image space and enjoy the same or even better image quality. This work is totally worth to do.
Thanks for reading, :)
說的大概意思是:
libjpeg是廣泛使用的開源JPEG圖像庫(kù),安卓也依賴libjpeg來(lái)壓縮圖片。但是安卓并不是直接封裝的libjpeg,而是基于了另一個(gè)叫Skia的開源項(xiàng)目來(lái)作為的圖像處理引擎。Skia是谷歌自己維 護(hù)著的一個(gè)大而全的引擎,各種圖像處理功能均在其中予以實(shí)現(xiàn),并且廣泛的應(yīng)用于谷歌自己和其它公司的產(chǎn)品中(如:Chrome、Firefox、 Android等)。Skia對(duì)libjpeg進(jìn)行了良好的封裝,基于這個(gè)引擎可以很方便為操作系統(tǒng)、瀏覽器等開發(fā)圖像處理功能。
libjpeg在壓縮圖像時(shí),有一個(gè)參數(shù)叫optimize_coding,關(guān)于這個(gè)參數(shù),libjpeg.doc有如下解釋:
就是上面那個(gè)解釋optimize_coding這段
這段話大概的意思就是如果設(shè)置optimize_coding為TRUE,將會(huì)使得壓縮圖像過程中基于圖像數(shù)據(jù)計(jì)算哈弗曼表(關(guān)于圖片壓縮中的哈弗曼表,請(qǐng)自行查閱相關(guān)資料),由于這個(gè)計(jì)算會(huì)顯著消耗空間和時(shí)間,默認(rèn)值被設(shè)置為FALSE。
谷歌的Skia項(xiàng)目工程師們最終沒有設(shè)置這個(gè)參數(shù),optimize_coding在Skia中默認(rèn)的等于了FALSE,這就意味著更差的圖片質(zhì)量和更大的圖片文件,而壓縮圖片過程中所耗費(fèi)的時(shí)間和空間其實(shí)反而是可以忽略不計(jì)的。那么,這個(gè)參數(shù)的影響究竟會(huì)有多大呢?
經(jīng)我們實(shí)測(cè),使用相同的原始圖片,分別設(shè)置optimize_coding=TRUE和FALSE進(jìn)行壓縮,想達(dá)到接近的圖片質(zhì)量(用Photoshop 放大到像素級(jí)逐塊對(duì)比),F(xiàn)ALSE時(shí)的圖片大小大約是TRUE時(shí)的5-10倍。換句話說,如果我們想在FALSE和TRUE時(shí)壓縮成相同大小的JPEG 圖片,F(xiàn)ALSE的品質(zhì)將大大遜色于TRUE的(雖然品質(zhì)很難量化,但我們不妨說成是差5-10倍)。
什么意思呢?意思就是現(xiàn)在設(shè)備發(fā)達(dá)啦,是時(shí)候?qū)ptimize_coding設(shè)置成true了,但是問題來(lái)了,Android系統(tǒng)代碼對(duì)于APP來(lái)說修改不了,我們有沒有什么辦法將這個(gè)參數(shù)進(jìn)行設(shè)置呢?答案肯定是有的,那就是自己使用自己的so庫(kù),不用系統(tǒng)的不就完了。
分析源碼
既然外國(guó)基友都說了是Android系統(tǒng)集成了這個(gè)庫(kù),但是參數(shù)沒設(shè)置好,咱也不明白為啥Android就是不改...但是我們也得驗(yàn)證一下外國(guó)基友說的對(duì)不對(duì)是吧。
那我們就從Bitmap.compress這個(gè)方法說起
public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
這個(gè)方法進(jìn)行質(zhì)量壓縮,而且可能失去alpha精度
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
return nativeCompress(mNativeBitmap, format.nativeInt, quality,
stream, new byte[WORKING_COMPRESS_STORAGE]);
}
我們看到quality只能是0-100的值
static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
int format, int quality,
jobject jstream, jbyteArray jstorage) {
SkImageEncoder::Type fm; //創(chuàng)建類型變量
//將java層類型變量轉(zhuǎn)換成Skia的類型變量
switch (format) {
case kJPEG_JavaEncodeFormat:
fm = SkImageEncoder::kJPEG_Type;
break;
case kPNG_JavaEncodeFormat:
fm = SkImageEncoder::kPNG_Type;
break;
case kWEBP_JavaEncodeFormat:
fm = SkImageEncoder::kWEBP_Type;
break;
default:
return false;
}
//判斷當(dāng)前bitmap指針是否為空
bool success = false;
if (NULL != bitmap) {
SkAutoLockPixels alp(*bitmap);
if (NULL == bitmap->getPixels()) {
return false;
}
//創(chuàng)建SkWStream變量用于將壓縮后的圖片數(shù)據(jù)輸出
SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
if (NULL == strm) {
return false;
}
//根據(jù)編碼類型,創(chuàng)建SkImageEncoder變量,并調(diào)用encodeStream對(duì)bitmap
//指針指向的圖片數(shù)據(jù)進(jìn)行編碼,完成后釋放資源。
SkImageEncoder* encoder = SkImageEncoder::Create(fm);
if (NULL != encoder) {
success = encoder->encodeStream(strm, *bitmap, quality);
delete encoder;
}
delete strm;
}
return success;
}
利用流和byte數(shù)組生成SkJavaOutputStream對(duì)象
SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage) {
static bool gInited;
if (!gInited) {
gInited = true;
}
return new SkJavaOutputStream(env, stream, storage);
}
bool SkImageEncoder::encodeStream(SkWStream* stream, const SkBitmap& bm,
int quality) {
quality = SkMin32(100, SkMax32(0, quality));
return this->onEncode(stream, bm, quality);
}
在SkImageEncoder中定義如下:
/**
* Encode bitmap 'bm' in the desired format, writing results to
* stream 'stream', at quality level 'quality' (which can be in
* range 0-100).
*
* This must be overridden by each SkImageEncoder implementation.
*/
virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) = 0;
但是總體來(lái)說,Android是使用skia庫(kù)的,我們同樣在源碼目錄下也能找到對(duì)應(yīng)位置:
external\skia
同樣我們觀察一個(gè)現(xiàn)象:
就是在SkImageEncoder中定義的onEncode函數(shù),是個(gè)virtual的,那我們應(yīng)該把她所有的實(shí)現(xiàn)類都找出來(lái)。
class SkKTXImageEncoder : public SkImageEncoder {}
class SkImageEncoder_CG : public SkImageEncoder {}
class SkPNGImageEncoder : public SkImageEncoder {}
class SkWEBPImageEncoder : public SkImageEncoder {}
class SkImageEncoder_WIC : public SkImageEncoder {}
class SkARGBImageEncoder : public SkImageEncoder {}
這么多類實(shí)現(xiàn)了這個(gè)接口而且他們都有個(gè)共同的路徑:
\external\skia\src\images
那我們就看看SkPNGImageEncoder中的onEncode方法是什么樣子
class SkJPEGImageEncoder : public SkImageEncoder {
protected:
virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) {
#ifdef TIME_ENCODE
SkAutoTime atm("JPEG Encode");
#endif
SkAutoLockPixels alp(bm);
if (NULL == bm.getPixels()) {
return false;
}
jpeg_compress_struct cinfo;//申請(qǐng)并初始化jpeg壓縮對(duì)象,同時(shí)要指定錯(cuò)誤處理器
skjpeg_error_mgr sk_err;// 聲明錯(cuò)誤處理器,并賦值給jcs.err域
skjpeg_destination_mgr sk_wstream(stream);
// allocate these before set call setjmp
SkAutoMalloc oneRow;
SkAutoLockColors ctLocker;
cinfo.err = jpeg_std_error(&sk_err);
sk_err.error_exit = skjpeg_error_exit;
if (setjmp(sk_err.fJmpBuf)) {
return false;
}
// Keep after setjmp or mark volatile.
const WriteScanline writer = ChooseWriter(bm);
if (NULL == writer) {
return false;
}
jpeg_create_compress(&cinfo);
cinfo.dest = &sk_wstream;
cinfo.image_width = bm.width();
cinfo.image_height = bm.height();
cinfo.input_components = 3;
#ifdef WE_CONVERT_TO_YUV
cinfo.in_color_space = JCS_YCbCr;
#else
cinfo.in_color_space = JCS_RGB;
#endif
cinfo.input_gamma = 1;
/**
jpeg_set_defaults函數(shù)一定要等設(shè)置好圖像寬、高、色彩通道數(shù)計(jì)色彩空間四個(gè)參數(shù)后才能調(diào)用,
因?yàn)檫@個(gè)函數(shù)要用到這四個(gè)值,調(diào)用jpeg_set_defaults函數(shù)后,jpeglib庫(kù)采用默認(rèn)的設(shè)置對(duì)圖像進(jìn)行壓縮,
如果需要改變?cè)O(shè)置,如壓縮質(zhì)量,調(diào)用這個(gè)函數(shù)后,可以調(diào)用其它設(shè)置函數(shù),如jpeg_set_quality函數(shù)。
其實(shí)圖像壓縮時(shí)有好多參數(shù)可以設(shè)置,但大部分我們都用不著設(shè)置,只需調(diào)用jpeg_set_defaults函數(shù)值為默認(rèn)值即可。
*/
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);//給cinfo中設(shè)置quality
#ifdef DCT_IFAST_SUPPORTED
cinfo.dct_method = JDCT_IFAST;
#endif
/*
上面的工作準(zhǔn)備完成后,就可以壓縮了,壓縮過程非常簡(jiǎn)單,首先調(diào)用jpeg_start_compress,然后可以對(duì)每一行進(jìn)行壓縮,
也可以對(duì)若干行進(jìn)行壓縮,甚至可以對(duì)整個(gè)的圖像進(jìn)行一次壓縮,壓縮完成后,記得要調(diào)用jpeg_finish_compress函數(shù)
*/
jpeg_start_compress(&cinfo, TRUE);//設(shè)置開始?jí)嚎s的必要天劍
const int width = bm.width();
uint8_t* oneRowP = (uint8_t*)oneRow.reset(width * 3);
const SkPMColor* colors = ctLocker.lockColors(bm);
const void* srcRow = bm.getPixels();
//下面是對(duì)每一行進(jìn)行壓縮
while (cinfo.next_scanline < cinfo.image_height) {
JSAMPROW row_pointer[1]; //一行位圖
writer(oneRowP, srcRow, width, colors);
row_pointer[0] = oneRowP;
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);//向壓縮容器中寫數(shù)據(jù)
srcRow = (const void*)((const char*)srcRow + bm.rowBytes());
}
//最后就是釋放壓縮工作過程中所申請(qǐng)的資源了,主要就是jpeg壓縮對(duì)象
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
return true;
}
};
里面牽扯到JCS_RGB,JCS_YCbCr
00206 typedef enum {
00207 JCS_UNKNOWN, /* error/unspecified */
00208 JCS_GRAYSCALE, /* monochrome */
00209 JCS_RGB, /* red/green/blue */
00210 JCS_YCbCr, /* Y/Cb/Cr (also known as YUV) */
00211 JCS_CMYK, /* C/M/Y/K */
00212 JCS_YCCK /* Y/Cb/Cr/K */
00213 } J_COLOR_SPACE;
//Definition at line 206 of file jpeglib.h.
而且我們看出來(lái)里面使用:
00217 typedef enum {
00218 JDCT_ISLOW, /* slow but accurate integer algorithm */
00219 JDCT_IFAST, /* faster, less accurate integer method */
00220 JDCT_FLOAT /* floating-point: accurate, fast on fast HW */
00221 } J_DCT_METHOD;
一種快但是不精準(zhǔn)的方法進(jìn)行變換。按照網(wǎng)上有關(guān)基友的說法:
link
1.Skia默認(rèn)先將圖片轉(zhuǎn)為YUV444格式,再進(jìn)行編碼(WE_CONVERT_TO_YUV宏默認(rèn)打開狀態(tài),否則就是先轉(zhuǎn)為RGB888格式,再傳入Jpeg編碼時(shí)轉(zhuǎn)YUV)
2.默認(rèn)使用JDCT_IFAST方法做傅立葉變換,很明顯會(huì)造成一定的圖片質(zhì)量損失(即使quality設(shè)成100也存在,是計(jì)算精度的問題)
jpeg_start_compress:
看文檔還是這只一些安全檢查所需要的參數(shù)為壓縮做準(zhǔn)備
/*
* Compression initialization.
* Before calling this, all parameters and a data destination must be set up.
*
* We require a write_all_tables parameter as a failsafe check when writing
* multiple datastreams from the same compression object. Since prior runs
* will have left all the tables marked sent_table=TRUE, a subsequent run
* would emit an abbreviated stream (no tables) by default. This may be what
* is wanted, but for safety's sake it should not be the default behavior:
* programmers should have to make a deliberate choice to emit abbreviated
* images. Therefore the documentation and examples should encourage people
* to pass write_all_tables=TRUE; then it will take active thought to do the
* wrong thing.
*/
jpeg_start_compress (j_compress_ptr cinfo, boolean write_all_tables)
{
if (cinfo->global_state != CSTATE_START)
ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);
if (write_all_tables)
jpeg_suppress_tables(cinfo, FALSE); /* mark all tables to be written */
/* (Re)initialize error mgr and destination modules */
(*cinfo->err->reset_error_mgr) ((j_common_ptr) cinfo);
(*cinfo->dest->init_destination) (cinfo);
/* Perform master selection of active modules */
jinit_compress_master(cinfo);
/* Set up for the first pass */
(*cinfo->master->prepare_for_pass) (cinfo);
/* Ready for application to drive first pass through jpeg_write_scanlines
* or jpeg_write_raw_data.
*/
cinfo->next_scanline = 0;
cinfo->global_state = (cinfo->raw_data_in ? CSTATE_RAW_OK : CSTATE_SCANNING);
}
至此壓縮就完成了,我們也就看出Android系統(tǒng)是通過libjpeg進(jìn)行壓縮的。
但是Android集成的libjpeg和我們使用的也有一些不一樣,所以我建議使用自己編譯開元so進(jìn)行操作,這樣可以根據(jù)我們需求來(lái)定制參數(shù)達(dá)到更好的符合我們項(xiàng)目的目的。
小結(jié):
我們已經(jīng)知道Android系統(tǒng)中是使用skia庫(kù)進(jìn)行壓縮的,skia庫(kù)中又是使用其他開元庫(kù)進(jìn)行壓縮對(duì)于jpg的壓縮就是使用libjpeg這個(gè)庫(kù)。
2. Android中有圖片所占內(nèi)存因素分析
我們經(jīng)常因?yàn)閳D片太大導(dǎo)致oom,但是很多小伙伴,只是借鑒網(wǎng)上的建議和方法,并不知道原因,那么我們接下來(lái)就大致分析一下圖片在Android中加載由那些因素決定呢?
getByteCount()
表示存儲(chǔ)bitmap像素所占內(nèi)存
public final int getByteCount() {
return getRowBytes() * getHeight();
}
getAllocationByteCount()
Returns the size of the allocated memory used to store this bitmap's pixels.
返回bitmap所占像素已經(jīng)分配的大小
This can be larger than the result of getByteCount() if a bitmap is reused to decode other bitmaps of smaller size, or by manual reconfiguration. See reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap. If a bitmap is not modified in this way, this value will be the same as that returned by getByteCount().
This value will not change over the lifetime of a Bitmap.
如果一個(gè)bitmap被復(fù)用更小尺寸的bitmap編碼,或者手工重新配置。那么實(shí)際尺寸可能偏小。具體看reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap.如果不牽扯復(fù)用否是新產(chǎn)生的,納悶就和getByteContent()相同。
這個(gè)值在bitmap生命周期內(nèi)不會(huì)改變
所以從代碼看mBuffer.length就是緩沖區(qū)真是長(zhǎng)度
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer 代表存儲(chǔ) Bitmap 像素?cái)?shù)據(jù)的字節(jié)數(shù)組。
return getByteCount();
}
return mBuffer.length;
}
然后我們看看占用內(nèi)存如何計(jì)算的
Bitamp 占用內(nèi)存大小 = 寬度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一個(gè)像素所占的內(nèi)存
那么一個(gè)像素占用的內(nèi)存多大呢?這個(gè)就和配置的規(guī)格有關(guān)系
SkBitmap.cpp
static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0, // Unknown
1, // Alpha_8
2, // RGB_565
2, // ARGB_4444
4, // RGBA_8888
4, // BGRA_8888
1, // kIndex_8
};
常用的就是RGBA_8888也就是一個(gè)像素占用四個(gè)字節(jié)大小
- ARGB_8888:每個(gè)像素占四個(gè)字節(jié),A、R、G、B 分量各占8位,是 Android 的默認(rèn)設(shè)置;
- RGB_565:每個(gè)像素占兩個(gè)字節(jié),R分量占5位,G分量占6位,B分量占5位;
- ARGB_4444:每個(gè)像素占兩個(gè)字節(jié),A、R、G、B分量各占4位,成像效果比較差;
- Alpha_8: 只保存透明度,共8位,1字節(jié);
于此同時(shí)呢,在BitmapFactory 的內(nèi)部類 Options 有兩個(gè)成員變量 inDensity 和 inTargetDensity其中
- inDensity 就 Bitmap 的像素密度,也就是 Bitmap 的成員變量 mDensity默認(rèn)是設(shè)備屏幕的像素密度,可以通過 Bitmap#setDensity(int) 設(shè)置
- inTargetDensity 是圖片的目標(biāo)像素密度,在加載圖片時(shí)就是 drawable 目錄的像素密度
當(dāng)資源加載的時(shí)候會(huì)進(jìn)行這兩個(gè)值的初始化
調(diào)用的是 BitmapFactory#decodeResource 方法,內(nèi)部調(diào)用的是 decodeResourceStream 方法
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//實(shí)際上,我們這里的opts是null的,所以在這里初始化。
/**
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
*/
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;//這里density的值如果對(duì)應(yīng)資源目錄為hdpi的話,就是240
}
}
//請(qǐng)注意,inTargetDensity就是當(dāng)前的顯示密度,比如三星s6時(shí)就是640
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
會(huì)根據(jù)設(shè)備屏幕像素密度到對(duì)應(yīng) drawable 目錄去尋找圖片,這個(gè)時(shí)候 inTargetDensity/inDensity = 1,圖片不會(huì)做縮放,寬度和高度就是圖片原始的像素規(guī)格,如果沒有找到,會(huì)到其他 drawable 目錄去找,這個(gè)時(shí)候 drawable 的屏幕像素密度就是 inTargetDensity,會(huì)根據(jù) inTargetDensity/inDensity 的比例對(duì)圖片的寬度和高度進(jìn)行縮放。
所以歸結(jié)上面影響圖片內(nèi)存的原因有:
- 色彩格式,前面我們已經(jīng)提到,如果是 ARGB8888 那么就是一個(gè)像素4個(gè)字節(jié),如果是 RGB565 那就是2個(gè)字節(jié)
- 原始文件存放的資源目錄
- 目標(biāo)屏幕的密度
- 圖片本身的大小
3.圖片的幾種壓縮辦法
- 質(zhì)量壓縮
public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
注意這種方式,是通過改變alpha通道,改變色彩度等方式達(dá)到壓縮圖片的目的,壓縮使得存儲(chǔ)大小變小,但是并不改變加載到內(nèi)存的大小,也就是說,如果你從1M壓縮到了1K,解壓縮出來(lái)在內(nèi)存中大小還是1M。而且有個(gè)很坑的問題,就是如果設(shè)置quality=100,這個(gè)圖片存儲(chǔ)大小會(huì)增大,而且會(huì)小幅度失真。具體原因,我在上面分析源碼的時(shí)候還沒仔細(xì)研究,初步判斷可能是利用傅里葉變換導(dǎo)致。
尺寸壓縮
尺寸壓縮在使用的時(shí)候BitmapFactory.Options 類型的參數(shù)當(dāng)置 BitmapFactory.Options.inJustDecodeBounds=true只讀取圖片首行寬高等信息,并不會(huì)將圖片加載到內(nèi)存中。設(shè)置 BitmapFactory.Options 的 inSampleSize 屬性可以真實(shí)的壓縮 Bitmap 占用的內(nèi)存,加載更小內(nèi)存的 Bitmap。
設(shè)置 inSampleSize 之后,Bitmap 的寬、高都會(huì)縮小 inSampleSize 倍。
inSampleSize 比1小的話會(huì)被當(dāng)做1,任何 inSampleSize 的值會(huì)被取接近2的冪值色彩模式壓縮
也就是我們?cè)谏誓J缴线M(jìn)行變換,通過設(shè)置通過 BitmapFactory.Options.inPreferredConfig改變不同的色彩模式,使得每個(gè)像素大小改變,從而圖片大小改變Matrix 矩陣變換
使用:
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
Matrix matrix = new Matrix();
float rate = computeScaleRate(bitmapWidth, bitmapHeight);
matrix.postScale(rate, rate);
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
其實(shí)這個(gè)操作并不是節(jié)省內(nèi)存,他只是結(jié)合我們對(duì)尺寸壓縮進(jìn)行補(bǔ)充,我們進(jìn)行尺寸壓縮之后難免不會(huì)滿足我們對(duì)尺寸的要求,所以我們就借助Matrix進(jìn)行矩陣變換,改變圖片的大小。
- Bitmap#createScaledBitmap
這個(gè)也是和Matrix一個(gè)道理,都是進(jìn)行縮放。不改變內(nèi)存。
3.圖片壓縮的最終解決方案
我們通過上面的總結(jié)我們歸納出,圖片的壓縮目的有兩種:
- 壓縮內(nèi)存,防止產(chǎn)生OOM
- 壓縮存儲(chǔ)空間,目的節(jié)約空間,但是解壓到內(nèi)存中大小不變。還是原來(lái)沒有壓縮圖片時(shí)候的大小。
那么我們應(yīng)該怎么壓縮才合理呢,其實(shí)這個(gè)需要根據(jù)需求來(lái)定,可能有人就會(huì)說我說的是廢話,但是事實(shí)如此。我提供一些建議:
- 使用libjpeg開源項(xiàng)目,不使用Android集成的libjpeg,因?yàn)槲覀兛梢愿鶕?jù)需要修改參數(shù),更符合我們項(xiàng)目的效果。
- 合理通過尺寸變換和矩陣變換在內(nèi)存上優(yōu)化。
- 對(duì)不同屏幕分辨率的機(jī)型壓縮進(jìn)行壓縮的程度不一樣。
那么我們就開始我們比較難的一個(gè)環(huán)節(jié)就是集成開源庫(kù)。
4.編譯libjpeg生成so庫(kù)
首先確保我們安裝了ndk環(huán)境,不管是Linux還是windows還是macOs都可以編譯,只要我們有ndk
-
我們必須知道我們NDK能夠使用,并且可以調(diào)用到我們ndk里面的工具,這就要求我們要配置環(huán)境變量,當(dāng)然Linux和windows不一樣,macOS由于我這種窮逼肯定買不起所以我也布吉島怎么弄。但是思想就是要能用到ndk工具
windows是在我們環(huán)境變量中進(jìn)行配置
-
Linux呢
echo "export ANDROID_HOME='Your android ndk path'" >> ~/.bash_profile source ~/.bash_profile當(dāng)然Linux還可以寫.sh來(lái)個(gè)腳本豈不更好
NDK=/opt/ndk/android-ndk-r12b/ PLATFORM=$NDK/platforms/android-15/arch-arm/ PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/ CC=$PREBUILT/bin/arm-linux-androideabi-gcc ./configure --prefix=/home/linc/jpeg-9b/jni/dist --host=arm CC="$CC --sysroot=$PLATFORM"最執(zhí)行寫的.sh
這個(gè)腳本是根據(jù)config文件寫的,那里面有我們需要的參數(shù)還有注釋,所以我們要能看懂那個(gè)才可以。一般情況出了問題我們?cè)谘芯磕莻€(gè)吧
引薦大牛方法
-
構(gòu)建libjpeg-turbo.so
cd ../libjpeg-turbo-android/libjpeg-turbo/jni ndk-build APP_ABI=armeabi-v7a,armeabi這個(gè)時(shí)候就可以得到libjpegpi.so在../libjpeg-turbo-android/libjpeg-turbo/libs/armeabi和armeabi-v7a目錄下
-
復(fù)制我們的libjpegpi.so到 ../bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni
cd ../bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni ndk-build 得到 libjpegpi.so and libpijni.so
jni使用的時(shí)候一定java的類名要和jni里面方法前面的單詞要對(duì)上
static {
System.loadLibrary("jpegpi");
System.loadLibrary("pijni");
}
所以如果不改項(xiàng)目的話類名必須為com.pi.common.util.NativeUtil
5.庫(kù)函數(shù)的介紹
net.bither.util.NativeUtil:
package net.bither.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.util.Log;
public class NativeUtil {
private static String Tag = NativeUtil.class.getSimpleName();
private static int DEFAULT_QUALITY = 95;
/**
* @Description: JNI基本壓縮
* @param bit
* bitmap對(duì)象
* @param fileName
* 指定保存目錄名
* @param optimize
* 是否采用哈弗曼表數(shù)據(jù)計(jì)算 品質(zhì)相差5-10倍
* @author XiaoSai
* @date 2016年3月23日 下午6:32:49
* @version V1.0.0
*/
public static void compressBitmap(Bitmap bit, String fileName, boolean optimize) {
saveBitmap(bit, DEFAULT_QUALITY, fileName, optimize);
}
/**
* @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄
* @param image
* bitmap對(duì)象
* @param filePath
* 要保存的指定目錄
* @author XiaoSai
* @date 2016年3月23日 下午6:28:15
* @version V1.0.0
*/
public static void compressBitmap(Bitmap image, String filePath) {
// 最大圖片大小 150KB
int maxSize = 150;
// 獲取尺寸壓縮倍數(shù)
int ratio = NativeUtil.getRatioSize(image.getWidth(),image.getHeight());
// 壓縮Bitmap到對(duì)應(yīng)尺寸
Bitmap result = Bitmap.createBitmap(image.getWidth() / ratio,image.getHeight() / ratio,Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, image.getWidth() / ratio, image.getHeight() / ratio);
canvas.drawBitmap(image,null,rect,null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質(zhì)量壓縮方法,這里100表示不壓縮,把壓縮后的數(shù)據(jù)存放到baos中
int options = 100;
result.compress(Bitmap.CompressFormat.JPEG, options, baos);
// 循環(huán)判斷如果壓縮后圖片是否大于100kb,大于繼續(xù)壓縮
while (baos.toByteArray().length / 1024 > maxSize) {
// 重置baos即清空baos
baos.reset();
// 每次都減少10
options -= 10;
// 這里壓縮options%,把壓縮后的數(shù)據(jù)存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, options, baos);
}
// JNI保存圖片到SD卡 這個(gè)關(guān)鍵
NativeUtil.saveBitmap(result, options, filePath, true);
// 釋放Bitmap
if (!result.isRecycled()) {
result.recycle();
}
}
/**
* @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄
* @param curFilePath
* 當(dāng)前圖片文件地址
* @param targetFilePath
* 要保存的圖片文件地址
* @author XiaoSai
* @date 2016年9月28日 下午17:43:15
* @version V1.0.0
*/
public static void compressBitmap(String curFilePath, String targetFilePath,int maxSize) {
//根據(jù)地址獲取bitmap
Bitmap result = getBitmapFromFile(curFilePath);
if(result==null){
Log.i(Tag,"result is null");
return;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質(zhì)量壓縮方法,這里100表示不壓縮,把壓縮后的數(shù)據(jù)存放到baos中
int quality = 100;
result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循環(huán)判斷如果壓縮后圖片是否大于100kb,大于繼續(xù)壓縮
while (baos.toByteArray().length / 1024 > maxSize) {
// 重置baos即清空baos
baos.reset();
// 每次都減少10
quality -= 10;
// 這里壓縮quality,把壓縮后的數(shù)據(jù)存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
}
// JNI保存圖片到SD卡 這個(gè)關(guān)鍵
NativeUtil.saveBitmap(result, quality, targetFilePath, true);
// 釋放Bitmap
if (!result.isRecycled()) {
result.recycle();
}
}
/**
* 計(jì)算縮放比
* @param bitWidth 當(dāng)前圖片寬度
* @param bitHeight 當(dāng)前圖片高度
* @return int 縮放比
* @author XiaoSai
* @date 2016年3月21日 下午3:03:38
* @version V1.0.0
*/
public static int getRatioSize(int bitWidth, int bitHeight) {
// 圖片最大分辨率
int imageHeight = 1280;
int imageWidth = 960;
// 縮放比
int ratio = 1;
// 縮放比,由于是固定比例縮放,只用高或者寬其中一個(gè)數(shù)據(jù)進(jìn)行計(jì)算即可
if (bitWidth > bitHeight && bitWidth > imageWidth) {
// 如果圖片寬度比高度大,以寬度為基準(zhǔn)
ratio = bitWidth / imageWidth;
} else if (bitWidth < bitHeight && bitHeight > imageHeight) {
// 如果圖片高度比寬度大,以高度為基準(zhǔn)
ratio = bitHeight / imageHeight;
}
// 最小比率為1
if (ratio <= 0)
ratio = 1;
return ratio;
}
/**
* 通過文件路徑讀獲取Bitmap防止OOM以及解決圖片旋轉(zhuǎn)問題
* @param filePath
* @return
*/
public static Bitmap getBitmapFromFile(String filePath){
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;//只讀邊,不讀內(nèi)容
BitmapFactory.decodeFile(filePath, newOpts);
int w = newOpts.outWidth;
int h = newOpts.outHeight;
// 獲取尺寸壓縮倍數(shù)
newOpts.inSampleSize = NativeUtil.getRatioSize(w,h);
newOpts.inJustDecodeBounds = false;//讀取所有內(nèi)容
newOpts.inDither = false;
newOpts.inPurgeable=true;//不采用抖動(dòng)解碼
newOpts.inInputShareable=true;//表示空間不夠可以被釋放,在5.0后被釋放
// newOpts.inTempStorage = new byte[32 * 1024];
Bitmap bitmap = null;
FileInputStream fs = null;
try {
fs = new FileInputStream(new File(filePath));
} catch (FileNotFoundException e) {
Log.i(Tag,"bitmap :"+e.getStackTrace());
e.printStackTrace();
}
try {
if(fs!=null){
bitmap = BitmapFactory.decodeFileDescriptor(fs.getFD(),null,newOpts);
//旋轉(zhuǎn)圖片
int photoDegree = readPictureDegree(filePath);
if(photoDegree != 0){
Matrix matrix = new Matrix();
matrix.postRotate(photoDegree);
// 創(chuàng)建新的圖片
bitmap = Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}else{
Log.i(Tag,"fs :null");
}
} catch (IOException e) {
e.printStackTrace();
} finally{
if(fs!=null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}
/**
*
* 讀取圖片屬性:旋轉(zhuǎn)的角度
* @param path 圖片絕對(duì)路徑
* @return degree旋轉(zhuǎn)的角度
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
/**
* 調(diào)用native方法
* @Description:函數(shù)描述
* @param bit
* @param quality
* @param fileName
* @param optimize
* @author XiaoSai
* @date 2016年3月23日 下午6:36:46
* @version V1.0.0
*/
private static void saveBitmap(Bitmap bit, int quality, String fileName, boolean optimize) {
compressBitmap(bit, bit.getWidth(), bit.getHeight(), quality, fileName.getBytes(), optimize);
}
/**
* 調(diào)用底層 bitherlibjni.c中的方法
* @Description:函數(shù)描述
* @param bit
* @param w
* @param h
* @param quality
* @param fileNameBytes
* @param optimize
* @return
* @author XiaoSai
* @date 2016年3月23日 下午6:35:53
* @version V1.0.0
*/
private static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
boolean optimize);
/**
* 加載lib下兩個(gè)so文件
*/
static {
System.loadLibrary("jpegbither");
System.loadLibrary("bitherjni");
}
}
所以我們最后的核心就是使用saveBitmap就會(huì)將圖片壓縮并且保存在sd卡上。而且我們獲取圖片的時(shí)候也對(duì)內(nèi)存做了判斷,防止產(chǎn)生oom
6.壓縮結(jié)果
一張5M,一張是140k但是我截圖看上去效果差不多,哈哈。也就是說,外國(guó)基友說的其實(shí)很有道理哈哈。

