Android Bitmap 使用

zeroXuan發表於2019-05-10

在日常開發中,可以說和Bitmap低頭不見抬頭見,基本上每個應用都會直接或間接的用到,而這裡面又涉及到大量的相關知識。 所以這裡把Bitmap的常用知識做個梳理,限於經驗和能力,不做太深入的分析。

Bitmap記憶體模型

  1. 在Android 2.2(API8)之前,當GC工作時,應用的執行緒會暫停工作,同步的GC會影響效能。而Android2.3之後,GC變成了併發的,意味著Bitmap沒有引用的時候其佔有的記憶體會很快被回收。
  2. 在Android 2.3.3(API10)之前,Bitmap的畫素資料存放在Native記憶體,而Bitmap物件本身則存放在Dalvik Heap中。Native記憶體中的畫素資料並不會以可預測的方式進行同步回收,有可能會導致記憶體升高甚至OOM。而在Android3.0之後,Bitmap的畫素資料也被放在了Dalvik Heap中。

Bitmap記憶體佔用

手動計算

計算Bitmap記憶體佔用分為兩種情況:

  1. 使用BitmapFactory.decodeResource()載入本地資原始檔的方式

    無論是使用decodeResource(Resources res, int id)還是使用decodeResource(Resources res, int id, BitmapFactory.Options opts)其記憶體佔用的計算方式都是: width * height * inTargetDensity / inDensity * inTargetDensity / inDensity * 一個畫素所佔的記憶體。

  2. 使用BitmapFactory.decodeResource()以外的方式,計算方式是: width * height *一個畫素所佔的記憶體。

所用引數解釋一下:

  • width:圖片的原始畫素寬度。
  • height:圖片的原始畫素高度。
  • inTargetDensity:目標裝置的螢幕密度,例如一臺手機的螢幕密度是640dp,那麼inTargetDensity的值就是640dp。
  • inDensity:這個值跟這張圖片的放置的目錄有關(比如 hdpi 是240,xxhdpi 是480)。
  • 一個畫素所佔的記憶體:使用Bitmap.Config來描述一個畫素所佔用的記憶體,Bitmap.Config有四個取值,分別是:
    • ARGB_8888: 每個畫素4位元組,每個通道8位,四通道共32位,圖片質量是最高的,但是佔用的記憶體也是最大的,是 預設設定
    • RGB_565:共16位,2位元組,只儲存RGB值,圖片失真小,沒有透明度,可用於不需要透明度是圖片。
    • Alpha_8: 只有A通道,沒有顏色值,即只儲存透明度,共8位,1位元組,可用於設定遮蓋效果。
    • ARGB_4444: ,每個通道均佔用4位,共16位,2位元組,嚴重失真,基本不使用。

Android API 的方法

getByteCount()

getByteCount()方法是在API12加入的,代表儲存Bitmap的色素需要的最少記憶體。API19開始getAllocationByteCount()方法代替了getByteCount()。

getAllocationByteCount()

API19之後,Bitmap加了一個Api:getAllocationByteCount();代表在記憶體中為Bitmap分配的記憶體大小。

public final int getAllocationByteCount() {
        if (mBuffer == null) {
            //mBuffer代表儲存Bitmap畫素資料的位元組陣列。
            return getByteCount();
        }
        return mBuffer.length;
    }
複製程式碼

getByteCount()與getAllocationByteCount()的區別

  • 一般情況下兩者是相等的;
  • 通過複用Bitmap來解碼圖片,如果被複用的Bitmap的記憶體比待分配記憶體的Bitmap大,那麼getByteCount()表示新解碼圖片佔用記憶體的大小(並非實際記憶體大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實佔用的記憶體大小(即mBuffer的長度)。

Bitmap的建立

通常我們可以利用Bitmap的靜態方法createBitmap()BitmapFactory的decode系列靜態方法建立Bitmap物件。

Bitmap.createBitmap

主要用於圖片的操作,例如圖片的縮放,裁剪等。

Android Bitmap 使用
Android Bitmap 使用

BitmapFactory

Android Bitmap 使用

注意decodeFiledecodeResource 其實最終都會呼叫 decodeStream 方法來解析Bitmap 。有一個特別有意思的事情是,在 decodeResource 呼叫 decodeStream 之前還會呼叫 decodeResourceStream 這個方法,這個方法主要對 Options進行處理,在得到opts.inDensity的屬性前提下,如果沒有對該屬性的設定值,那麼opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;這個值預設為標準dpi的基值:160。如果沒有設定opts.inTargetDensity的值時,opts.inTargetDensity = res.getDisplayMetrics().densityDpi; 該值為當前裝置的 densityDpi,這個值是根據你放置在 drawable 下的檔案不同而不同的。所以說 decodeResourceStream 這個方法主要對 opts.inDensity 和 opts.inTargetDensity進行賦值。

儘量不要使用setImageBitmapsetImageResourceBitmapFactory.decodeResource來設定一張大圖,因為這些函式在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體,可以通過BitmapFactory.decodeStream方法,建立出一個bitmap,再將其設為ImageView的 source。

Resource資源載入的方式相當的耗費記憶體,建議採用通過InputStream ins = resources.openRawResource(resourcesId);然後使用decodeStream代替decodeResource獲取Bitmap。這麼做的好處是:

  • BitmapFactory.decodeResource 載入的圖片可能會經過縮放,該縮放目前是放在 java 層做的,效率比較低,而且需要消耗 java 層的記憶體。因此,如果大量使用該介面載入圖片,容易導致OOM錯誤。
  • BitmapFactory.decodeStream 不會對所載入的圖片進行縮放,相比之下佔用記憶體少,效率更高。

這兩個介面各有用處,如果對效能要求較高,則應該使用 decodeStream;如果對效能要求不高,且需要 Android 自帶的圖片自適應縮放功能,則可以使用 decodeResource。

Bitmap 於 drawable 的相互轉換

Bitmap 轉 drawable

Drawable newBitmapDrawable = new BitmapDrawable(bitmap);
還可以從BitmapDrawable中獲取Bitmap物件
Bitmap bitmap = new BitmapDrawable.getBitmap();
複製程式碼

drawable 轉 Bitmap

  1. BitmapFactory 中的 decodeResource 方法

    Resources res = getResources();
    Bitmap    bmp = BitmapFactory.decodeResource(res, R.drawable.ic_drawable);
    複製程式碼
  2. 將 Drable 物件先轉化成 BitmapDrawable ,然後呼叫 getBitmap 方法 獲取

    Resource res      = gerResource();
    Drawable drawable = res.getDrawable(R.drawable.ic_drawable);//獲取drawable
    BitmapDrawable bd = (BitmapDrawable) drawable;
    Bitmap bm         = bd.getBitmap();
    複製程式碼
  3. 根據已有的Drawable建立一個新的Bitmap

    public static Bitmap drawableToBitmap(Drawable drawable) {
    
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        System.out.println("Drawable轉Bitmap");
        Bitmap.Config config =
                drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
                        : Bitmap.Config.RGB_565;
                        
        Bitmap bitmap = Bitmap.createBitmap(w, h, config);
        
        //注意,下面三行程式碼要用到,否則在View或者SurfaceView裡的canvas.drawBitmap會看不到圖
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, w, h);
        drawable.draw(canvas);
    
        return bitmap;
    }
    複製程式碼

BitmapFactory.Options的屬性解析

  • inJustDecodeBounds:如果這個值為 true ,那麼在解碼的時候將不會返回 Bitmap ,只會返回這個 Bitmap 的尺寸。這個屬性的目的是,如果你只想知道一個 Bitmap 的尺寸,但又不想將其載入到記憶體中時,是一個非常好用的屬性。
  • outWidth和outHeight:表示這個 Bitmap 的寬和高,一般和 inJustDecodeBounds 一起使用來獲得 Bitmap的寬高,但是不載入到記憶體。
  • inSampleSize:壓縮圖片時取樣率的值,如果這個值大於1,那麼就會按照比例(1 / inSampleSize)來縮小 Bitmap 的寬和高。如果這個值為 2,那麼 Bitmap 的寬為原來的1/2,高為原來的1/2,那麼這個 Bitmap 是所佔記憶體畫素值會縮小為原來的 1/4。
  • inDensity:表示這個 Bitmap 的畫素密度,對應的是 DisplayMetrics 中的 densityDpi,不是 density。(如果不明白它倆之間的異同,可以看我的 Android 螢幕各種引數的介紹和學習 )
  • inTargetDensity:表示要被新 Bitmap 的目標畫素密度,對應的是 DisplayMetrics 中的 densityDpi。
  • inScreenDensity:表示實際裝置的畫素密度,對應的是 DisplayMetrics 中的 densityDpi。
  • inPreferredConfig:這個值是設定色彩模式,預設值是 ARGB_8888,這個模式下,一個畫素點佔用 4Byte 。RGB_565 佔用 2Byte,ARGB_4444 佔用 4Byte(以廢棄)。
  • inPremultiplied:這個值和透明度通道有關,預設值是 true,如果設定為 true,則返回的 Bitmap 的顏色通道上會預先附加上透明度通道。
  • inScaled:設定這個Bitmap 是否可以被縮放,預設值是 true,表示可以被縮放。
  • inMutable:若為true,則返回的Bitmap是可變的,可以作為Canvas的底層Bitmap使用。 若為false,則返回的Bitmap是不可變的,只能進行讀操作。 如果要修改Bitmap,那就必須返回可變的bitmap,例如:修改某個畫素的顏色值(setPixel)
  • inBitmap:這個引數用來實現 Bitmap 記憶體的複用,但複用存在一些限制,具體體現在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的記憶體,而 Android 4.4 及以後版本則只要後來的 Bitmap 比之前的小即可。使用 inBitmap 引數前,每建立一個 Bitmap 物件都會分配一塊記憶體供其使用,而使用了 inBitmap 引數後,多個 Bitmap 可以複用一塊記憶體,這樣可以提高效能。

Bitmap如何複用

Android Bitmap 使用

Android Bitmap 使用

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

  • Bitmap複用首選需要其 mIsMutable 屬性為 true , mIsMutable 的表面意思為:易變的

    在Bitmap中的意思為: 控制bitmap的setPixel方法能否使用,也就是外界能否修改bitmap的畫素。mIsMutable 屬性為 true 那麼就可以修改Bitmap的畫素資料,這樣也就可以實現Bitmap物件的複用了。

  • 在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100,那麼新申請的bitmap必須也為100-100才能夠被重用。

  • 被複用的Bitmap必須是Mutable,即inMutable的值為true。違反此限制,不會丟擲異常,且會返回新申請記憶體的Bitmap。

  • 從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小。違反此限制,將會導致複用失敗,丟擲異常IllegalArgumentException(Problem decoding into existing bitmap)

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

Bitmap如何壓縮

質量壓縮

質量壓縮不會改變圖片的畫素點,即我們使用完質量壓縮後,在轉換Bitmap時佔用記憶體依舊不會減小。但是可以減少我們儲存在本地檔案的大小,即放到 disk上的大小。

/**
     * 質量壓縮方法,並不能減小載入到記憶體時所佔用記憶體的空間,應該是減小的所佔用磁碟的空間
     * @param image
     * @param compressFormat
     * @return
     */
    public static Bitmap compressbyQuality(Bitmap image, Bitmap.CompressFormat compressFormat) {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //質量壓縮方法,這裡100表示不壓縮,把壓縮後的資料存放到baos中
        image.compress(compressFormat, 100, baos);
        int quality = 100;

        //迴圈判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
        while ( baos.toByteArray().length / 1024 > 100) { 
            baos.reset();//重置baos即清空baos
            if(quality > 10){
                quality -= 20;//每次都減少20
            }else {
                break;
            }
            
            //這裡壓縮options%,把壓縮後的資料存放到baos中
            image.compress(Bitmap.CompressFormat.JPEG,quality,baos);
        }
        
        //把壓縮後的資料baos存放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        
        //把ByteArrayInputStream資料生成圖片
        Bitmap bmp = BitmapFactory.decodeStream(isBm, null, options);

        return bmp;
    }
複製程式碼

取樣壓縮

這個方法主要用在圖片資源本身較大,或者適當地取樣並不會影響視覺效果的條件下,這時候我們輸出的目標可能相對的較小,對圖片的大小和解析度都減小。

壓縮格式 CompressFormat

  • Bitmap.CompressFormat.JPEG
    • 一種有失真壓縮(JPEG2000既可以有損也可以無損),".jpg"或者".jpeg";
    • 優點:採用了直接色,有豐富的色彩,適合儲存照片和生動影像效果;缺點:有損,不適合用來儲存logo、線框類圖
  • Bitmap.CompressFormat.PNG
    • 一種無失真壓縮,".png";
    • PNG 格式是無損的,它無法再進行質量壓縮,quality 這個引數就沒有作用了,會被忽略,所以最後圖片儲存成的檔案大小不會有變化;
    • 優點:支援透明、無損,主要用於小圖示,透明背景等;
    • 缺點:若色彩複雜,則圖片生成後檔案很大;
  • Bitmap.CompressFormat.WEBP
    • 以WebP演算法進行壓縮;
    • Google開發的新的圖片格式,同時支援無損和有失真壓縮,使用直接色。
    • 無失真壓縮,相同質量的webp比PNG小大約26%;
    • 有失真壓縮,相同質量的webp比JPEG小25%-34% 支援動圖,基本取代gif
    • 缺點:解壓速度慢
    **
     * 取樣率壓縮,這個和矩陣來實現縮放有點類似,但是有一個原則是“大圖小用用取樣,小圖大用用矩陣”。
     * 也可以先用取樣來壓縮圖片,這樣記憶體小了,可是圖的尺寸也小。如果要是用 Canvas 來繪製這張圖時,再用矩陣放大
     * @param image
     * @param compressFormat
     * @param requestWidth 要求的寬度
     * @param requestHeight 要求的長度
     * @return
     */
    public static Bitmap compressbySample(Bitmap image, Bitmap.CompressFormat compressFormat, int requestWidth, int requestHeight){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        //質量壓縮方法,這裡100表示不壓縮,把壓縮後的資料存放到baos中
        image.compress(compressFormat,100,baos);
        
        //把壓縮後的資料baos存放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inPurgeable = true;
        
        //只讀取圖片的頭資訊,不去解析真是的點陣圖
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(isBm,null,options);
        options.inSampleSize = calculateInSampleSize(options,requestWidth,requestHeight);
        
        //-------------inBitmap------------------
        options.inMutable = true;
        try{
            Bitmap inBitmap = Bitmap.createBitmap(options.outWidth, options.outHeight, Bitmap.Config.RGB_565);
            if (inBitmap != null && canUseForInBitmap(inBitmap, options)) {
                options.inBitmap = inBitmap;
            }
        }catch (OutOfMemoryError e){
            options.inBitmap = null;
            System.gc();
        }

        //---------------------------------------

        options.inJustDecodeBounds = false;//真正的解析點陣圖
        
        isBm.reset();
        Bitmap compressBitmap;
        try{
            compressBitmap =  BitmapFactory.decodeStream(isBm, null, options);//把ByteArrayInputStream資料生成圖片
        }catch (OutOfMemoryError e){
            compressBitmap = null;
            System.gc();
        }

        return compressBitmap;
    }

    /**
     * 取樣壓縮比例
     * @param options
     * @param reqWidth 要求的寬度
     * @param reqHeight 要求的長度
     * @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){
            // 計算出實際寬高和目標寬高的比率
            final int heightRatio = Math.round((float) originalHeight / (float) reqHeight);
            final int widthRatio = Math.round((float) originalWidth / (float) reqWidth);
            // 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
            // 一定都會大於等於目標的寬和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;

        }
        return inSampleSize;
    }
複製程式碼

使用矩陣

前面我們採用了取樣壓縮,Bitmap 所佔用的記憶體是小了,可是圖的尺寸也小了。當我們需要尺寸較大時該怎麼辦?我們要用用 Canvas 繪製怎麼辦?當然可以用矩陣(Matrix)

/**
     * 矩陣縮放圖片
     * @param sourceBitmap
     * @param width 要縮放到的寬度
     * @param height 要縮放到的長度
     * @return
     */
    private Bitmap getScaleBitmap(Bitmap sourceBitmap,float width,float height){
        Bitmap scaleBitmap;
        //定義矩陣物件
        Matrix matrix = new Matrix();
        float scale_x = width/sourceBitmap.getWidth();
        float scale_y = height/sourceBitmap.getHeight();
        matrix.postScale(scale_x,scale_y);

        try {
            scaleBitmap = Bitmap.createBitmap(sourceBitmap,0,0,sourceBitmap.getWidth(),sourceBitmap.getHeight(),matrix,true);
        }catch (OutOfMemoryError e){
            scaleBitmap = null;
            System.gc();
        }
        return scaleBitmap;
    }
複製程式碼

相關文章