Android Bitmap 使用

在平常開發中,能夠說和Bitmap低頭不見擡頭見,基本上每一個應用都會直接或間接的用到,而這裏面又涉及到大量的相關知識。 因此這裏把Bitmap的經常使用知識作個梳理,限於經驗和能力,不作太深刻的分析。java

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內存佔用分爲兩種狀況:web

  1. 使用BitmapFactory.decodeResource()加載本地資源文件的方式算法

    不管是使用decodeResource(Resources res, int id)仍是使用decodeResource(Resources res, int id, BitmapFactory.Options opts)其內存佔用的計算方式都是: width * height * inTargetDensity / inDensity * inTargetDensity / inDensity * 一個像素所佔的內存。canvas

  2. 使用BitmapFactory.decodeResource()之外的方式,計算方式是: width * height *一個像素所佔的內存。數組

所用參數解釋一下:bash

  • 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對象。post

Bitmap.createBitmap

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

BitmapFactory

注意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如何複用

使用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;
    }
複製代碼
相關文章
相關標籤/搜索