1. 程式人生 > >Android Bitmap 優化- 圖片壓縮

Android Bitmap 優化- 圖片壓縮

一直以來Bitmap都是開發中很棘手的問題,這個問題就是傳說中的OOM(java.lang.OutofMemoryError - 記憶體溢位),那麼Bitmap為何如此喪失,令無數Android開發者所懊惱?

一、Bitmap引發OOM的原因

  1. 由於每個機型在編譯ROM時都設定了一個應用堆記憶體VM值上限dalvik.vm.heapgrowthlimit,用來限定每個應用可用的最大記憶體,超出這個最大值將會報OOM。這個閥值,一般根據手機螢幕dpi大小遞增,dpi越小的手機,每個應用可用最大記憶體就越低。例如我的Z3c,xhdpi的VM閥值是192M,但是到了nexus s hdpi上只有可憐的48M。這樣,當一個activity中載入多張大圖後,就很容易OOM了。有關應用記憶體閥值的參考可以看這裡3.7節:

    http://static.googleusercontent.com/media/source.android.com/en//compatibility/android-cdd.pdf

  2. 圖片解析度越高,消耗的記憶體越高,當載入高解析度圖片的時候,將會非常佔用記憶體,一旦處理不當就會OOM。例如,一張500W畫素的照片的解析度是:2592x1936。如果Bitmap使用 ARGB_8888 32位來平鋪顯示的話,佔用的記憶體是2592x1936x4個位元組,佔用將近19M記憶體,my god,載入不到10張這種高質量照片,應用將直接掛掉,報OOM

  3. 在使用ListView, GridView等這些大量載入view的元件時,如果沒有合理的處理快取,大量載入Bitmap的時候,也將容易引發OOM

二、介紹Bitmap

工欲善其事必先利其器,想要高效載入Bitmap,瞭解Bitmap是必不可少的。Bitmap有幾個重要的成員變數和方法,下面開始介紹:

2.1 Bitmap.Config

一張圖片Bitmap所佔用的記憶體 = 圖片長度 x 圖片寬度 x 一個畫素點佔用的位元組數
而Bitmap.Config,正是指定單位畫素佔用的位元組數的重要引數。

其中,A代表透明度;R代表紅色;G代表綠色;B代表藍色。

  • ALPHA_8
    表示8位Alpha點陣圖,即A=8,一個畫素點佔用1個位元組,它沒有顏色,只有透明度
  • ARGB_4444
    表示16位ARGB點陣圖,即A=4,R=4,G=4,B=4,一個畫素點佔4+4+4+4=16位,2個位元組
  • ARGB_8888
    表示32位ARGB點陣圖,即A=8,R=8,G=8,B=8,一個畫素點佔8+8+8+8=32位,4個位元組
  • RGB_565
    表示16位RGB點陣圖,即R=5,G=6,B=5,它沒有透明度,一個畫素點佔5+6+5=16位,2個位元組

Bitmap.Config主要作用是:以何種方式畫素儲存。不同的配置將會影響影象的畫質(色彩深度),位數越高畫質越高,顯然在這裡ARGB_8888是最佔記憶體的。當然,畫質越高也就越佔記憶體了。

Tips:由於ARGB_4444的畫質慘不忍睹,一般假如對圖片沒有透明度要求的話,可以改成RGB_565,相比ARGB_8888將節省一半的記憶體開銷。

2.1.1 配置不同Bitmap.Config在相同解析度下的佔用記憶體情況

一張圖片Bitmap所佔用的記憶體 = 圖片長度 x 圖片寬度 x 一個畫素點佔用的位元組數

Bitmap.Config 解析度100x100的圖片佔用記憶體的大小
ALPHA_8 100x100x1 = 10000 byte ~= 9.77 KB
ARGB_4444 100x100x2 = 20000 byte ~= 19.53 kb
ARGB_8888 100x100x4 = 40000 byte ~= 39.06 KB
RGB_565 100x100x2 = 20000 byte ~= 19.53 KB

在Android裡面可以通過下面的程式碼來設定解位元速率

2.2 Bitmap.CompressFormat

從字面上理解,它的含義是:Bitmap壓縮格式

public enum CompressFormat {
    JPEG    (0),
    PNG     (1),
    WEBP    (2);

    CompressFormat(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    final int nativeInt;
}

嗯,其實這個引數很簡單,就是指定Bitmap是以JPEG、PNG還是WEBP格式來壓縮

2.3 Bitmap.compress()方法

重磅方法來了,通過這個方法,可以實現圖片的壓縮。使用該方法需要傳三個引數進去:CompressFormat、int型別的quality、OutputStream

  • CompressFormat
    指定Bitmap的壓縮格式,可選擇JPEG、PNG、WEBP
  • int型別的quality
    指定Bitmap的壓縮品質,範圍是0 ~ 100;該值越高,畫質越高。0表示畫質最差,100畫質最高。

  • OutputStream
    指定Bitmap的位元組輸出流。一般使用:

    ByteArrayOutputStream stream = new ByteArrayOutputStream();

    // Bitmap.compress()方法
    public boolean compress(CompressFormat format, 
    int quality, OutputStream stream) {
    
        if (stream == null) {
            throw new NullPointerException();
        }
        if (quality < 0 || quality > 100) {
            throw new IllegalArgumentException("quality must be 0..100");
        }
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
        boolean result = nativeCompress(mFinalizer.mNativeBitmap, format.nativeInt,
                quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        return result;
    }
    

2.3.1 案例:將一個Bitmap壓縮成jpeg, quality為10,程式碼如下:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_test3);

    ImageView iv_1 = (ImageView) findViewById(R.id.iv_1);
    ImageView iv_2 = (ImageView) findViewById(R.id.iv_2);

    Bitmap bmp = BitmapFactory.
    decodeResource(this.getResources(), R.mipmap.test_pic);

    iv_1.setImageBitmap(bmp);

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    bmp.compress(Bitmap.CompressFormat.JPEG, 10, bos);

    byte[] bytes = bos.toByteArray();
    bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

    iv_2.setImageBitmap(bmp);
}

執行效果圖:【下面那張圖明顯要比上一張畫質差了很多】

三、介紹BitmapFactory

從上面那個案例的程式碼可以發現,獲取Bitmap不是通過構造new出來的,而是通過BitmapFactory”製造”出來的。BitmapFactory是獲取Bitmap和壓縮Bitmap的重要類,下面開始介紹BitmapFactory幾個重要的成員變數和方法:

3.1 通過BitmapFactory解碼(獲取)Bitmap的幾種方式

  • decodeFile()   //從SD卡檔案讀取

    Bitmap bm = BitmapFactory.decodeFile(Environment.
    getExternalStorageDirectory().getAbsolutePath()+"/photo.jpg");
    
  • decodeResource()   //從資原始檔res讀取

    Bitmap bm = BitmapFactory.
    decodeResource(this.getResources(), R.mipmap.test_pic);
    
  • decodeStream()   //從輸入流讀取

    Bitmap bm = BitmapFactory.decodeStream(inputStream);
    
  • decodeByteArray()   //從位元組陣列讀取

    Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    

3.2 BitmapFactory.Options

BitmapFactory在使用方法decodeFile()、decodeResource()解碼圖片時,可以指定它的BitmapFactory.Options。這個引數作用非常大,它可以設定Bitmap的取樣率,通過改變圖片的寬度、高度、縮放比例等,以達到降低圖片的畫素的目的,這樣可以做到圖片壓縮,減少Bitmap的記憶體

下面列出BitmapFactory.Options的部分成員變數:

public Bitmap inBitmap;

public boolean inJustDecodeBounds;

public int inSampleSize;

public int inDensity;

public int inTargetDensity;

public int inScreenDensity;

public boolean inScaled;

public int outWidth;

public int outHeight;

public String outMimeType;

看到這麼多成員變數是不是傻了?no,no,no,其實很簡單。一句話總結:in開頭的代表的就是設定某某引數;out開頭的代表的就是獲取某某引數。比如,inSampleSize就是設定Bitmap的縮放比例、outWidth就是獲取Bitmap的高度。

3.2.1 inJustDecodeBounds 設定只去讀圖片的附加資訊(寬高),不去解析真實的Bitmap

從字面上理解,它的含義是:”設定僅解碼Bitmap的邊界”。那它真正的作用是啥呢?

當inJustDecodeBounds設定為true的時候,BitmapFactory通過decodeResource或者decodeFile解碼圖片時,將會返回空(null)的Bitmap物件,這樣可以避免Bitmap的記憶體分配,但是它可以返回Bitmap的寬度、高度以及MimeType。

// 當inJustDecodeBounds設定為true時,獲取Bitmap的寬度、高度以及MimeType
BitmapFactory.Options options =  new BitmapFactory.Options(); 
options.inJustDecodeBounds =  true ; 
BitmapFactory.decodeResource (getResources(), R.id.myimage, options); 
int imageHeight = options.outHeight ; 
int imageWidth = options.outWidth ; 
String imageType = options.outMimeType ;

那麼這樣做有何意義呢?看完下面這段程式碼,你就知道這樣做有啥意義了。意義就在於,可以先不用產生Bitmap記憶體,從而獲得圖片的寬高資訊,儘可能的做到節約記憶體。

3.2.1.1 通過BitmapFactory.Options根據手機螢幕尺寸設定圖片的縮放比例

// 根據手機螢幕尺寸設定圖片的縮放比例【將大圖縮放】
public class TestThreadActivity3 extends Activity {

@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_test3);

    ImageView iv_1 = (ImageView) findViewById(R.id.iv_1);
    ImageView iv_2 = (ImageView) findViewById(R.id.iv_2);

    BitmapFactory.Options opts = new BitmapFactory.Options();

    opts.inJustDecodeBounds = true;  //只去讀圖片的頭資訊,不去解析真實的點陣圖      
    Bitmap bmp = BitmapFactory.decodeResource(this.getResources(),
     R.mipmap.test_pic2,opts);

    WindowManager wm  = getWindowManager();

    int screenWidth = wm.getDefaultDisplay().getWidth();//得到螢幕的寬度
    int screenheight = wm.getDefaultDisplay().getHeight();//得到螢幕的高度

    Log.e("螢幕寬度:",screenWidth+"");
    Log.e("螢幕高度:", screenheight + "");

    int picWidth = opts.outWidth;// 得到圖片寬度
    int picHeight = opts.outHeight;// 得到圖片高度
    Log.e("原圖片高度:",picHeight+"");
    Log.e("原圖片寬度:", picWidth + "");

    //計算圖片縮放比例
    int dx = picWidth/screenWidth;
    int dy = picHeight/screenheight;
    Log.e("dx,dy",dx+","+dy+"");
    int scale = 1;
    if(dx>=dy&&dy>=1){
        Log.e("按照水平方向縮放:" ,dx+"");
        scale = dx;
    }
    if(dy>dx&&dx>=1){
        Log.e("按照豎直方向縮放:", dy + "");
        scale = dy;
    }

    opts.inSampleSize = scale;//設定縮放比例
    opts.inJustDecodeBounds = false;//真正的去解析點陣圖
    bmp = BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic2,opts);
    int picWidth2 = opts.outWidth;// 得到圖片寬度
    int picHeight2 = opts.outHeight;// 得到圖片高度
    Log.e("壓縮後的圖片寬度:",picWidth2+"");
    Log.e("壓縮後的圖片高度:", picHeight2 + "");
    Log.e("壓縮後的圖佔用記憶體:",bmp.getByteCount()+"");
    iv_2.setImageBitmap(bmp);
}

}

我們讀取一張3840x2400的圖片執行結果:
原圖直接佔用36M記憶體,如果直接設定的話將瞬間爆炸報OOM。所以我們這裡先不載入Bitmap,而是隻獲取寬和高,待縮放後,再進行真實的載入Bitmap。

3.2.2 inSampleSize 設定圖片的縮放比例(寬和高)

在這裡著重講一下這個inSampleSize。從字面上理解,它的含義是:”設定取樣大小“。它的作用是:

設定inSampleSize的值(int型別)後,假如設為4,則寬和高都為原來的1/4,寬高都減少了,自然記憶體也降低了。

如圖所示:

如何理解”設定取樣大小“呢?如果你認真看了上面的內容話,聰明的你一定知道,肯定需要配合inJustDecodeBounds,先獲取圖片的寬、高【這個過程就是取樣】,然後通過獲取的寬高,動態的設定inSampleSize的值。

【當然,你也可以不動態,可以寫死inSampleSize的值。比如設定inSampleSize = 4的話,一張解析度為2048x1536px的影象將使用inSampleSize值為4的設定來解碼,產生的Bitmap大小約為512*384px。相較於完整圖片佔用12M的記憶體,這種方式只需0.75M記憶體(假設Bitmap配置為ARGB_8888)。】

這裡再舉例演示一個動態設定inSampleSize的案例程式碼,可以通過設定圖片寬高來縮放圖片尺寸:

public class TestThreadActivity3 extends Activity {

@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_test3);

    ImageView iv_1 = (ImageView) findViewById(R.id.iv_1);
    ImageView iv_2 = (ImageView) findViewById(R.id.iv_2);

    BitmapFactory.Options opts = new BitmapFactory.Options();

    opts.inJustDecodeBounds = true;   //只去讀圖片的附加資訊,不去解析真實的點陣圖

    Bitmap bmp = BitmapFactory.
    decodeResource(this.getResources(), R.mipmap.test_pic,opts);


    Log.e("原圖佔用記憶體:", bmp.getByteCount() + "");
    iv_1.setImageBitmap(bmp);

    int picWidth = opts.outWidth;// 得到圖片寬度
    int picHeight = opts.outHeight;// 得到圖片高度

    Log.e("原圖片高度:",picHeight+"");
    Log.e("原圖片寬度:",picWidth+"");

      //根據100*100的寬高,設定縮放比例
    opts.inSampleSize = calculateInSampleSize(opts,100,100);
    opts.inJustDecodeBounds = false;//真正的去解析點陣圖

    bmp = BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic, opts);
    Log.e("壓縮後的圖佔用記憶體:",bmp.getByteCount()+"");
    iv_2.setImageBitmap(bmp);
}

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {

    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
    Log.e("inSampleSize:",inSampleSize+"");
    return inSampleSize;
}

}

將圖片壓縮成100x100px解析度的執行結果:

3.2.3 inBitmap 重用Bitmap

inBitmap的主要作用是複用之前bitmap在記憶體中申請的記憶體,其實這是物件池的原理,以解決物件頻繁建立再回收的效率問題。

使用inBitmap前,每建立一個bitmap需要獨佔一塊記憶體

使用inBitmap後,多個bitmap會複用同一塊記憶體

所以使用inBitmap能夠大大提高記憶體的利用效率,但是它也有幾個限制條件:

  • inBitmap只能在3.0以後使用。在2.3上,bitmap的資料是儲存在native C的記憶體區域,並不是在java dalvik的記憶體堆上。

  • 在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100,那麼新申請的bitmap必須也為100-100才能夠被重用。從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小。

  • 新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那麼就不能支援4444與565格式的bitmap了,不過可以通過建立一個包含多種典型可重用bitmap的物件池,這樣後續的bitmap建立都能夠找到合適的“模板”去進行重用。

下面是如何使用inBitmap的程式碼示例:

四、總結(Bitmap壓縮的幾種方法)

1、Bitmap壓縮的兩種常用方法

  • 質量壓縮法 Bitmap.compress()
    參考2.3.1節的程式碼
  • 取樣壓縮法 設定inSampleSize的值
    參考3.2.1.1 和 3.2.2的程式碼

2、在實際使用中可以結合質量壓縮法和取樣壓縮法一起用,以達到最佳壓縮效果。

3、看完了這篇內容,其實說白了,Bitmap壓縮都是圍繞這個來做文章:Bitmap所佔用的記憶體 = 圖片長度 x 圖片寬度 x 一個畫素點佔用的位元組數。3個引數,任意減少一個的值,就達到了壓縮的效果。