Android圖片壓縮終結(jié)篇

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


qq_pic_merged_1506931428106.jpg

簡介

由于工作原因,boss下達(dá)的任務(wù)就大概說了對圖片進(jìn)行壓縮尋找比較合理的方式,還舉了一個項目中的坑排苍,就是系統(tǒng)原生的Bitmap.compress設(shè)置質(zhì)量參數(shù)為100生成圖片會變大的坑。所以我打算用一點時間研究研究Bitmap在內(nèi)存和外存中的情況税弃。
首先需要對圖片進(jìn)行壓縮纪岁,大家都知道圖片是Android里面一個大坑,具體的問題有

  • oom则果,一不留神就用OOM來沖沖喜幔翰,所以網(wǎng)上就有了很多解決oom問題的建議,但是由于網(wǎng)友的水平不一也導(dǎo)致建議參差不齊西壮。(內(nèi)存)
  • 圖片壓縮再加載失真嚴(yán)重遗增,或者壓縮率不夠達(dá)不到項目要求的效果。(外存)

那我今天就要解決的就是通過今天查閱的資料和自己的判斷款青,還有實踐歸檔一下圖片在Android上的問題做修。并且給出自己解決圖片壓縮問題的解決方案和實際操作。

1. 為什么Android上的圖片就不如IOS上的抡草?

我在全球男性交友中心找到了一個外國基友寫的一段情書:
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圖像庫饰及,安卓也依賴libjpeg來壓縮圖片。但是安卓并不是直接封裝的libjpeg康震,而是基于了另一個叫Skia的開源項目來作為的圖像處理引擎燎含。Skia是谷歌自己維 護(hù)著的一個大而全的引擎,各種圖像處理功能均在其中予以實現(xiàn)腿短,并且廣泛的應(yīng)用于谷歌自己和其它公司的產(chǎn)品中(如:Chrome屏箍、Firefox、 Android等)橘忱。Skia對libjpeg進(jìn)行了良好的封裝赴魁,基于這個引擎可以很方便為操作系統(tǒng)、瀏覽器等開發(fā)圖像處理功能钝诚。
libjpeg在壓縮圖像時颖御,有一個參數(shù)叫optimize_coding,關(guān)于這個參數(shù)凝颇,libjpeg.doc有如下解釋:

就是上面那個解釋optimize_coding這段

這段話大概的意思就是如果設(shè)置optimize_coding為TRUE潘拱,將會使得壓縮圖像過程中基于圖像數(shù)據(jù)計算哈弗曼表(關(guān)于圖片壓縮中的哈弗曼表秉继,請自行查閱相關(guān)資料),由于這個計算會顯著消耗空間和時間泽铛,默認(rèn)值被設(shè)置為FALSE。

谷歌的Skia項目工程師們最終沒有設(shè)置這個參數(shù)辑鲤,optimize_coding在Skia中默認(rèn)的等于了FALSE盔腔,這就意味著更差的圖片質(zhì)量和更大的圖片文件,而壓縮圖片過程中所耗費的時間和空間其實反而是可以忽略不計的月褥。那么弛随,這個參數(shù)的影響究竟會有多大呢?
經(jīng)我們實測宁赤,使用相同的原始圖片舀透,分別設(shè)置optimize_coding=TRUE和FALSE進(jìn)行壓縮,想達(dá)到接近的圖片質(zhì)量(用Photoshop 放大到像素級逐塊對比)决左,F(xiàn)ALSE時的圖片大小大約是TRUE時的5-10倍愕够。換句話說,如果我們想在FALSE和TRUE時壓縮成相同大小的JPEG 圖片佛猛,F(xiàn)ALSE的品質(zhì)將大大遜色于TRUE的(雖然品質(zhì)很難量化惑芭,但我們不妨說成是差5-10倍)。

什么意思呢继找?意思就是現(xiàn)在設(shè)備發(fā)達(dá)啦遂跟,是時候?qū)ptimize_coding設(shè)置成true了,但是問題來了婴渡,Android系統(tǒng)代碼對于APP來說修改不了幻锁,我們有沒有什么辦法將這個參數(shù)進(jìn)行設(shè)置呢?答案肯定是有的边臼,那就是自己使用自己的so庫哄尔,不用系統(tǒng)的不就完了。

分析源碼

既然外國基友都說了是Android系統(tǒng)集成了這個庫硼瓣,但是參數(shù)沒設(shè)置好究飞,咱也不明白為啥Android就是不改...但是我們也得驗證一下外國基友說的對不對是吧。

那我們就從Bitmap.compress這個方法說起

public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)

這個方法進(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對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對象

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;

但是總體來說,Android是使用skia庫的半哟,我們同樣在源碼目錄下也能找到對應(yīng)位置:

external\skia

同樣我們觀察一個現(xiàn)象:

就是在SkImageEncoder中定義的onEncode函數(shù)酬滤,是個virtual的签餐,那我們應(yīng)該把她所有的實現(xiàn)類都找出來。

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 {}

這么多類實現(xiàn)了這個接口而且他們都有個共同的路徑:

\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;//申請并初始化jpeg壓縮對象盯串,同時要指定錯誤處理器
        skjpeg_error_mgr        sk_err;// 聲明錯誤處理器氯檐,并賦值給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ù)計色彩空間四個參數(shù)后才能調(diào)用冠摄,
    因為這個函數(shù)要用到這四個值,調(diào)用jpeg_set_defaults函數(shù)后几缭,jpeglib庫采用默認(rèn)的設(shè)置對圖像進(jìn)行壓縮河泳,
    如果需要改變設(shè)置,如壓縮質(zhì)量年栓,調(diào)用這個函數(shù)后拆挥,可以調(diào)用其它設(shè)置函數(shù),如jpeg_set_quality函數(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)備完成后食拜,就可以壓縮了,壓縮過程非常簡單副编,首先調(diào)用jpeg_start_compress负甸,然后可以對每一行進(jìn)行壓縮,
    也可以對若干行進(jìn)行壓縮痹届,甚至可以對整個的圖像進(jìn)行一次壓縮呻待,壓縮完成后,記得要調(diào)用jpeg_finish_compress函數(shù)
    */

        jpeg_start_compress(&cinfo, TRUE);//設(shè)置開始壓縮的必要天劍

        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();
        //下面是對每一行進(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());
        }
        //最后就是釋放壓縮工作過程中所申請的資源了队腐,主要就是jpeg壓縮對象
        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.

而且我們看出來里面使用:

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編碼時轉(zhuǎn)YUV)
2.默認(rèn)使用JDCT_IFAST方法做傅立葉變換为严,很明顯會造成一定的圖片質(zhì)量損失(即使quality設(shè)成100也存在敛熬,是計算精度的問題)

jpeg_start_compress:

link

看文檔還是這只一些安全檢查所需要的參數(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ù)我們需求來定制參數(shù)達(dá)到更好的符合我們項目的目的。

小結(jié):

我們已經(jīng)知道Android系統(tǒng)中是使用skia庫進(jìn)行壓縮的诲锹,skia庫中又是使用其他開元庫進(jìn)行壓縮對于jpg的壓縮就是使用libjpeg這個庫繁仁。

2. Android中有圖片所占內(nèi)存因素分析

有個大仙分析的很好借用成果

我們經(jīng)常因為圖片太大導(dǎo)致oom,但是很多小伙伴归园,只是借鑒網(wǎng)上的建議和方法黄虱,并不知道原因,那么我們接下來就大致分析一下圖片在Android中加載由那些因素決定呢庸诱?

getByteCount()
表示存儲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.

如果一個bitmap被復(fù)用更小尺寸的bitmap編碼悬钳,或者手工重新配置说敏。那么實際尺寸可能偏小冀瓦。具體看reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap.如果不牽扯復(fù)用否是新產(chǎn)生的宣鄙,納悶就和getByteContent()相同。

這個值在bitmap生命周期內(nèi)不會改變

所以從代碼看mBuffer.length就是緩沖區(qū)真是長度

public final int getAllocationByteCount() {
    if (mBuffer == null) {
        //mBuffer 代表存儲 Bitmap 像素數(shù)據(jù)的字節(jié)數(shù)組聚谁。
        return getByteCount();
    }
    return mBuffer.length;
}

然后我們看看占用內(nèi)存如何計算的

Bitamp 占用內(nèi)存大小 = 寬度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一個像素所占的內(nèi)存

那么一個像素占用的內(nèi)存多大呢?這個就和配置的規(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也就是一個像素占用四個字節(jié)大小

  • ARGB_8888:每個像素占四個字節(jié)滞诺,A形导、R、G习霹、B 分量各占8位朵耕,是 Android 的默認(rèn)設(shè)置;
  • RGB_565:每個像素占兩個字節(jié)淋叶,R分量占5位阎曹,G分量占6位,B分量占5位煞檩;
  • ARGB_4444:每個像素占兩個字節(jié)处嫌,A、R斟湃、G熏迹、B分量各占4位,成像效果比較差凝赛;
  • Alpha_8: 只保存透明度注暗,共8位,1字節(jié)墓猎;

于此同時呢捆昏,在BitmapFactory 的內(nèi)部類 Options 有兩個成員變量 inDensity 和 inTargetDensity其中

  • inDensity 就 Bitmap 的像素密度,也就是 Bitmap 的成員變量 mDensity默認(rèn)是設(shè)備屏幕的像素密度毙沾,可以通過 Bitmap#setDensity(int) 設(shè)置
  • inTargetDensity 是圖片的目標(biāo)像素密度屡立,在加載圖片時就是 drawable 目錄的像素密度

當(dāng)資源加載的時候會進(jìn)行這兩個值的初始化

調(diào)用的是 BitmapFactory#decodeResource 方法,內(nèi)部調(diào)用的是 decodeResourceStream 方法

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
          InputStream is, Rect pad, Options opts) {
      //實際上,我們這里的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的值如果對應(yīng)資源目錄為hdpi的話,就是240
          }
      }
      //請注意焚刺,inTargetDensity就是當(dāng)前的顯示密度敛摘,比如三星s6時就是640
      if (opts.inTargetDensity == 0 && res != null) {
          opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
      }

      return decodeStream(is, pad, opts);
  }

會根據(jù)設(shè)備屏幕像素密度到對應(yīng) drawable 目錄去尋找圖片,這個時候 inTargetDensity/inDensity = 1乳愉,圖片不會做縮放兄淫,寬度和高度就是圖片原始的像素規(guī)格,如果沒有找到蔓姚,會到其他 drawable 目錄去找捕虽,這個時候 drawable 的屏幕像素密度就是 inTargetDensity,會根據(jù) inTargetDensity/inDensity 的比例對圖片的寬度和高度進(jìn)行縮放坡脐。

所以歸結(jié)上面影響圖片內(nèi)存的原因有:

  • 色彩格式泄私,前面我們已經(jīng)提到,如果是 ARGB8888 那么就是一個像素4個字節(jié)备闲,如果是 RGB565 那就是2個字節(jié)
  • 原始文件存放的資源目錄
  • 目標(biāo)屏幕的密度
  • 圖片本身的大小

3.圖片的幾種壓縮辦法

  1. 質(zhì)量壓縮

public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)

注意這種方式晌端,是通過改變alpha通道,改變色彩度等方式達(dá)到壓縮圖片的目的恬砂,壓縮使得存儲大小變小咧纠,但是并不改變加載到內(nèi)存的大小,也就是說泻骤,如果你從1M壓縮到了1K漆羔,解壓縮出來在內(nèi)存中大小還是1M。而且有個很坑的問題狱掂,就是如果設(shè)置quality=100钧椰,這個圖片存儲大小會增大,而且會小幅度失真符欠。具體原因嫡霞,我在上面分析源碼的時候還沒仔細(xì)研究,初步判斷可能是利用傅里葉變換導(dǎo)致希柿。

  1. 尺寸壓縮
    尺寸壓縮在使用的時候BitmapFactory.Options 類型的參數(shù)當(dāng)置 BitmapFactory.Options.inJustDecodeBounds=true只讀取圖片首行寬高等信息诊沪,并不會將圖片加載到內(nèi)存中。設(shè)置 BitmapFactory.Options 的 inSampleSize 屬性可以真實的壓縮 Bitmap 占用的內(nèi)存曾撤,加載更小內(nèi)存的 Bitmap端姚。
    設(shè)置 inSampleSize 之后,Bitmap 的寬挤悉、高都會縮小 inSampleSize 倍渐裸。
    inSampleSize 比1小的話會被當(dāng)做1,任何 inSampleSize 的值會被取接近2的冪值

  2. 色彩模式壓縮
    也就是我們在色彩模式上進(jìn)行變換,通過設(shè)置通過 BitmapFactory.Options.inPreferredConfig改變不同的色彩模式昏鹃,使得每個像素大小改變尚氛,從而圖片大小改變

  3. 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);

其實這個操作并不是節(jié)省內(nèi)存,他只是結(jié)合我們對尺寸壓縮進(jìn)行補充洞渤,我們進(jìn)行尺寸壓縮之后難免不會滿足我們對尺寸的要求阅嘶,所以我們就借助Matrix進(jìn)行矩陣變換,改變圖片的大小载迄。

  1. Bitmap#createScaledBitmap

這個也是和Matrix一個道理讯柔,都是進(jìn)行縮放。不改變內(nèi)存护昧。

3.圖片壓縮的最終解決方案

我們通過上面的總結(jié)我們歸納出魂迄,圖片的壓縮目的有兩種:

  • 壓縮內(nèi)存,防止產(chǎn)生OOM
  • 壓縮存儲空間惋耙,目的節(jié)約空間捣炬,但是解壓到內(nèi)存中大小不變。還是原來沒有壓縮圖片時候的大小怠晴。

那么我們應(yīng)該怎么壓縮才合理呢,其實這個需要根據(jù)需求來定浴捆,可能有人就會說我說的是廢話蒜田,但是事實如此。我提供一些建議:

  • 使用libjpeg開源項目选泻,不使用Android集成的libjpeg冲粤,因為我們可以根據(jù)需要修改參數(shù),更符合我們項目的效果页眯。
  • 合理通過尺寸變換和矩陣變換在內(nèi)存上優(yōu)化梯捕。
  • 對不同屏幕分辨率的機型壓縮進(jìn)行壓縮的程度不一樣。

那么我們就開始我們比較難的一個環(huán)節(jié)就是集成開源庫窝撵。

4.編譯libjpeg生成so庫

libjpeg項目下載地址

  1. 首先確保我們安裝了ndk環(huán)境傀顾,不管是Linux還是windows還是macOs都可以編譯,只要我們有ndk

  2. 我們必須知道我們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來個腳本豈不更好

      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

      這個腳本是根據(jù)config文件寫的,那里面有我們需要的參數(shù)還有注釋魁兼,所以我們要能看懂那個才可以婉徘。一般情況出了問題我們在研究那個吧
      引薦大牛方法

  3. 構(gòu)建libjpeg-turbo.so

    cd ../libjpeg-turbo-android/libjpeg-turbo/jni
    ndk-build APP_ABI=armeabi-v7a,armeabi
    

    這個時候就可以得到libjpegpi.so在../libjpeg-turbo-android/libjpeg-turbo/libs/armeabi和armeabi-v7a目錄下

  4. 復(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
    
  5. 得到 libjpegpi.so and libpijni.so

  6. jni使用的時候一定java的類名要和jni里面方法前面的單詞要對上

 static {

    System.loadLibrary("jpegpi");
   
    System.loadLibrary("pijni");

 }
 
所以如果不改項目的話類名必須為com.pi.common.util.NativeUtil

5.庫函數(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對象
     * @param fileName
     *            指定保存目錄名
     * @param optimize
     *            是否采用哈弗曼表數(shù)據(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對象
     * @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到對應(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卡 這個關(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卡 這個關(guān)鍵
        NativeUtil.saveBitmap(result, quality, targetFilePath, true);
        // 釋放Bitmap
        if (!result.isRecycled()) {
            result.recycle();
        }

    }

    /**
     * 計算縮放比
     * @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;
        // 縮放比,由于是固定比例縮放挺身,只用高或者寬其中一個數(shù)據(jù)進(jìn)行計算即可
        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;//不采用抖動解碼
        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 圖片絕對路徑
     * @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下兩個so文件
     */
    static {
        System.loadLibrary("jpegbither");
        System.loadLibrary("bitherjni");
    }

}

所以我們最后的核心就是使用saveBitmap就會將圖片壓縮并且保存在sd卡上章钾。而且我們獲取圖片的時候也對內(nèi)存做了判斷,防止產(chǎn)生oom

6.壓縮結(jié)果

一張5M热芹,一張是140k但是我截圖看上去效果差不多贱傀,哈哈。也就是說伊脓,外國基友說的其實很有道理哈哈府寒。

Screenshot_2017-10-02-15-46-26-071_com.miui.galle.png
Screenshot_2017-10-02-15-46-15-675_com.miui.galle.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市报腔,隨后出現(xiàn)的幾起案子株搔,更是在濱河造成了極大的恐慌,老刑警劉巖纯蛾,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纤房,死亡現(xiàn)場離奇詭異,居然都是意外死亡翻诉,警方通過查閱死者的電腦和手機炮姨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碰煌,“玉大人舒岸,你說我怎么就攤上這事÷” “怎么了蛾派?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長个少。 經(jīng)常有香客問我碍脏,道長,這世上最難降的妖魔是什么稍算? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任典尾,我火速辦了婚禮,結(jié)果婚禮上糊探,老公的妹妹穿的比我還像新娘钾埂。我一直安慰自己河闰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布褥紫。 她就那樣靜靜地躺著姜性,像睡著了一般。 火紅的嫁衣襯著肌膚如雪髓考。 梳的紋絲不亂的頭發(fā)上部念,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音氨菇,去河邊找鬼儡炼。 笑死,一個胖子當(dāng)著我的面吹牛查蓉,可吹牛的內(nèi)容都是我干的乌询。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼豌研,長吁一口氣:“原來是場噩夢啊……” “哼妹田!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鹃共,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤鬼佣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霜浴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晶衷,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年坷随,在試婚紗的時候發(fā)現(xiàn)自己被綠了房铭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驻龟。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡温眉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翁狐,到底是詐尸還是另有隱情类溢,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布露懒,位于F島的核電站闯冷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏懈词。R本人自食惡果不足惜蛇耀,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坎弯。 院中可真熱鬧纺涤,春花似錦译暂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拧咳,卻和暖如春伯顶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背骆膝。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工祭衩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谭网。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓汪厨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親愉择。 傳聞我的和親對象是個殘疾皇子劫乱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容