1. 程式人生 > >詳解Bitmap尺寸壓縮與質量壓縮

詳解Bitmap尺寸壓縮與質量壓縮

轉載請註明出自flowsky37的部落格,尊重他人辛苦勞動!

在Android系統中,關於圖片處理的是一個既常見又比較棘手的問題。一個應用中,存在需要展示大量的圖片的佈局,那我們必須要小心翼翼的處理了,不然OOM會就像一個定時炸彈一樣出現。由於大量點陣圖載入導致的記憶體溢位是Android中記憶體溢位常見場景之一。關於圖片的處理,一般情況下就是要對bitmap進行合適的處理跟優化。

在Android應用中,圖片的主要存在方式:

  • 以File的形式存在於SD卡中
  • 以Stream的形式存在於記憶體中
  • 以Bitmap形式存在於記憶體中

在SD卡中圖片佔用的記憶體與以Stream形式大小一樣,均小於Bitmap形式下記憶體佔比。既當圖片從SD卡中以流的形式載入到記憶體中時大小是不會發生變化的,但是stream轉化為Bitmap時,其大小會突然變大。這也是為什麼當需要載入大量圖片時容易出現OOM的主要原因。

BitmapFactory提供了4類方法載入Bitmap物件:

 1. decodeFile 從檔案中載入Bitmap物件
 2. decodeResource 從資源中載入Bitmap物件
 3. decodeStream 從輸入流中載入Bitmap物件
 4. decodeByteArray 從位元組陣列中載入Bitmap物件

其中decodeFile和decodeResource間接的呼叫了decodeStream方法。

為什麼轉化為Bitmap時大小會突然變大呢?

以任意一張圖片為例,我本地存了一張解析度為750*1334,大小為119K。如果將這張圖片以bitmap形式載入到記憶體中,它佔用的大小是多少,如何計算呢?它的計算公式:

圖片的佔用記憶體 = 圖片的長度(畫素單位) * 圖片的寬度(畫素單位) * 單位畫素所佔位元組數

其中單位佔用位元組數的大小是變化的,由BitmapFactory.Options的inPreferredConfig
決定,為Bitmap.Config型別,一般情況下預設為:ARGB_8888。不同型別佔位元組大小如下表所示:

這裡寫圖片描述

可以計算出此圖片以bitmap形式載入消耗記憶體為:750*1334*4=3.81M,還是由於我測試的圖片任意找的一張圖,現在手機拍的高清圖基本上都是2000+ * 2000+ * 4 = 15M+ 了。如果不做任何處理的圖片大量使用到我們APP上,結局你懂得!

現在對於圖片處理基本兩大方式:

  • 尺寸壓縮

尺寸壓縮會改變圖片的尺寸,即壓縮圖片寬度和高度的畫素點,從上面的計算公式我們可以得知,這樣會降低圖片由Stream轉化為bitmap記憶體的佔用,從而一定程度上減少OOM的概率。但是要注意,如果壓縮比太大,也會由於畫素點降低導致圖片失真嚴重,最後圖片有高清成了馬賽克。

如何進行尺寸壓縮呢?就是採用BitmapFactory.Options來載入所需要的尺寸。記住,是載入所需要合適的尺寸!例如一個ImageView顯示圖片,基本上ImageView所需要的尺寸不需要原始圖片那麼大,這時候我們就需要對圖片進行一些必要的處理。按照ImageView尺寸大小,通過BitmapFactory.Options來設定恰當的inSampleSize引數值來壓縮圖片,顯示在ImageView上,既避免了出現了OOM,也提高了Bitmap的載入效能。BitmapFactory提供了載入圖片的四個方法都支援BitmapFactory.Options引數,通過他們就可以實現對圖片進行壓縮處理。

通過BitmapF.Options壓縮圖片的核心就在於inSampleSize引數,取樣率或者取樣率。取樣率為整數,且為2的n次冪,n可以為0。即取樣率為1,處理後的圖片尺寸與原圖一致。當取樣率為2時,即寬、高均為原來的1/2,畫素則為原來的1/4.其佔有記憶體也為原來的1/4。當設定的取樣率小於1時,其效果與1一樣。當設定的inSampleSize大於1,不為2的指數時,系統會向下取一個最接近2的指數的值。

舉個例子:
當imageView的尺寸為200*300,,圖片的原始尺寸為600 * 900,則壓縮比為3即可。但是如果圖片尺寸為1000 * 1800呢?如果取樣率設定為5,縮放後的圖片尺寸為 200*360,此時還是能接受的。但是如果取樣率設定為6,縮放後的圖片會小於200 * 300。此時圖片會被拉伸導致變形。

壓縮圖片及獲取取樣率常規步驟:

 1. 將BitmapFactory.Options的inJustDecodeBounds引數設定為true載入圖片。
 2. 從BitmapFactory.Options中獲取圖片原始的寬高(outWidth/outHeight)值。
 3. 根據取樣率規則並結合所需要大小計算出合適的inSampleSize。
 4. BitmapFactory.Options引數設定為false,然後重新載入圖片。

在上面4步處理後圖片基本上就是縮放後的圖片了。解釋一下inJustDecodeBounds引數,當設定為true時,BitmapFactory只會解析圖片的原始寬/高資訊,不會真正的去載入圖片,所以這個操作是輕量級的。

示例程式碼:

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

public class ImageUtil {


    /**
     * 根據目標View的尺寸壓縮圖片返回bitmap
     * @param resources
     * @param resId
     * @param width 目標view的寬
     * @param height 目標view的高
     * @return
     */
    public static Bitmap decodeBitmapFromResource(Resources resources, int resId,int width ,int height){

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

        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(resources,resId,options);
        //獲取取樣率
        options.inSampleSize = calculateInSampleSize(options,width,height);

        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeResource(resources,resId,options);

    }

    /**
     * 獲取取樣率
     * @param options
     * @param reqWidth 目標view的寬
     * @param reqHeight 目標view的高
     * @return 取樣率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {

        int originalWidth = options.outWidth;
        int originalHeight = options.outHeight;

        int inSampleSize = 1;


        if (originalHeight > reqHeight || originalWidth > reqHeight){
            int halfHeight = originalHeight / 2;
            int halfWidth = originalWidth / 2;
            //壓縮後的尺寸與所需的尺寸進行比較
            while ((halfWidth / inSampleSize) >= reqHeight && (halfHeight /inSampleSize)>=reqWidth){
                inSampleSize *= 2;
            }

        }

        return inSampleSize;



    }
}

上面示例程式碼中採用的是decodeResource方法,其它幾個方法基本類似.

  • 質量壓縮

    質量壓縮不會改變圖片的畫素點,即前面我們說到的在轉化為bitmap時佔用記憶體變大時狀況不會得到改善。但是能方便的設定壓縮百分比,達到我們需要的大小。還是先看方法:
    Bitmap.compress(CompressFormat format, int quality, OutputStream stream)

    簡單解釋一下這三個引數,第一個表示Bitmap被壓縮成的圖片格式;第二個表示壓縮的質量控制,範圍0~100,很好理解。quality為80,表示壓縮為原來80%的質量效果。有些格式,例如png,它是無損的,這個值設定了也是無效的。因為質量壓縮是在保持畫素前提下改變圖片的位深及透明度等來壓縮圖片的,所以quality值與最後生成的圖片的大小並不是線性關係,比如大小為 300k的圖片,當quality為90時,得到的圖片大小並不是為270K。大家通過程式碼測試一下就會發現了。

    核心程式碼:

/**
     * 質量壓縮並存到SD卡中
     * @param bitmap
     * @param reqSize 需要的大小
     * @return
     */

    public static String qualityCompress1(Bitmap bitmap ,int reqSize){

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //這裡100表示不壓縮,把壓縮後的資料存放到baos中
        bitmap.compress(Bitmap.CompressFormat.JPEG,100,baos);
        int options = 95;
        //如果壓縮後的大小超出所要求的,繼續壓縮
        while (baos.toByteArray().length / 1024 > reqSize){
            baos.reset();
            bitmap.compress(Bitmap.CompressFormat.JPEG,options,baos);

            //每次減少5%質量
            if (options>5){//避免出現options<=0
                options -=5;
            } else {
                break;
            }

        }


        //存入SD卡中
        SimpleDateFormat formatYMD = new SimpleDateFormat("yyyy/MM/dd");

        String compressImgUri = LOCAL_URL + "000/"
                + formatYMD.format(new Date()) + "/" + System.currentTimeMillis() + "a.jpg";

        File outputFile = new File(compressImgUri);
            if (!outputFile.exists()) {
                outputFile.getParentFile().mkdirs();
            } else {
                outputFile.delete();
            }
            FileOutputStream out = null;
            try {
                out = new FileOutputStream(outputFile);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

            bitmap.compress(Bitmap.CompressFormat.JPEG, options, out);

            return outputFile.getPath();

    }

程式碼相當簡單,前面基本解釋過了。

圖片很多時壓縮的過程比較耗時,不要放到主執行緒執行。由於質量壓縮並不會減少圖片轉換為bitmap時的記憶體消耗,避免出現OOM,建議先進行合適的尺寸壓縮,然後再進一步進行質量壓縮。

如果有不明白的,可以留言;如果某些地方有誤,非常歡迎指出,謝謝!

專案地址:點選下載