天天看點

Bitmap避免OOM

目錄介紹

  • 01.先看一個需求分析案例
  • 02.Bitmap占用記憶體介紹
  • 03.影響Bitmap占用記憶體因素
  • 04.圖像加載的方式
  • 05.加載圖像記憶體去哪裡了
  • 06.具體實作加載圖檔步驟

希望世麟兄弟母親盡快好起來

  • 和南塵(世麟)認識與網絡,估計好多程式員都看過他的文章,我也是。雖然沒有見過面,但是微信聊過多次,感覺人非常不錯。都具有共同的愛好,喜歡寫技術部落格和開源項目,算得上同道中人。
  • 對于這次他遇到的事情,我的确很佩服他那種承擔的責任和勇氣。對于任何一個出身一般的人來說,大多數家庭都是難以承擔疾病所帶來的費用,即使有勇氣承擔,那經濟上也的确讓人壓力很大,如果可以盡綿薄之力,那就特别感謝大家呢!
  • 雖然大多數技術平台發這個水滴籌,有些人會表示不了解,有的甚至說會影響社群氛圍。我覺得這種擔心很正常,但是有點把問題放大了,首先這個是一個特别小機率的事件,并不會存在說大家都這樣做就造成不好的影響。其次有人還說,會過渡消費社會這種同情和愛心,這種擔心挺好,但是人總是會有分辨是非的能力,如果能夠幫忙那就盡綿薄之力,如果不能幫忙那也不要亂扣帽子。
  • 我知道最近世麟心情是錯綜複雜,但還好的是絕大多數程式員都是特别有善心的。我始終覺得一個能夠堅持寫技術部落格,而且還寫了這麼多,掘金還是以前的掘金,沒有發生變化。
  • 程式員爸爸癱瘓14年,媽媽又這樣,幫幫南塵!

  • 案例說明
    • 加載一個本地的大圖檔或者網絡圖檔,從加載到設定到View上,如何減下記憶體,避免加載圖檔OOM。
  • 案例分析
    • 在展示高分辨率圖檔的時候,最好先将圖檔進行壓縮。壓縮後的圖檔大小應該和用來展示它的控件大小相近,在一個很小的ImageView上顯示一張超大的圖檔不會帶來任何視覺上的好處,但卻會占用相當多寶貴的記憶體,而且在性能上還可能會帶來負面影響。

  • 網絡圖檔計算Bitmap的記憶體大小
    • bitmap記憶體大小 = 圖檔長度 x 圖檔寬度 x 機關像素占用的位元組數
    • 起決定因素就是最後那個參數了,Bitmap'常見有2種編碼方式:ARGB_8888和RGB_565,ARGB_8888每個像素點4個byte,RGB_565是2個byte,一般都采用ARGB_8888這種。那麼常見的1080*1920的圖檔記憶體占用就是:1920 x 1080 x 4 = 7.9M
  • 加載本地資源計算Bitmap的記憶體大小
    • 加載一張本地資源圖檔,那麼它占用的記憶體 = width height nTargetDensity/inDensity 一個像素所占的記憶體。
    • 詳細可以看這篇文章 04.Bitmap計算記憶體
  • 正确說法,這個注意呢?計算公式如下所示
    • 對資源檔案:width height nTargetDensity/inDensity nTargetDensity/inDensity 一個像素所占的記憶體;
    • 别的:width height 一個像素所占的記憶體;

  • 影響Bitmap占用記憶體的因素:
    • 圖檔最終加載的分辨率;
    • 圖檔的格式(PNG/JPEG/BMP/WebP);
    • 圖檔所存放的drawable目錄;
    • 圖檔屬性設定的色彩模式;
    • 裝置的螢幕密度;

  • 擷取圖像的來源一般有三種源頭:
    • 1.從網絡加載2.從檔案讀取3.從資源檔案加載
  • 針對這三種情況我們一般使用BitmapFactory的
    • decodeStream,decodeFile,decodeResource,這三個函數來擷取到bitmap然後再調用ImageView的setImageBitmap函數進行展現。

  • 思考一下:記憶體去哪裡了(為什麼被消耗了這麼多)?
    • 其實我們的記憶體就是去bitmap裡了,BitmapFactory的每個decode函數都會生成一個bitmap對象,用于存放解碼後的圖像,然後傳回該引用。如果圖像資料較大就會造成bitmap對象申請的記憶體較多,如果圖像過多就會造成記憶體不夠用自然就會出現out of memory的現象。
  • 為何容易OOM?
    • 通過BitmapFactory的decode的這些方法會嘗試為已經建構的bitmap配置設定記憶體,這時就會很容易導緻OOM出現。為此每一種解析方法都提供了一個可選的BitmapFactory.Options參數,将這個參數的inJustDecodeBounds屬性設定為true就可以讓解析方法禁止為bitmap配置設定記憶體,傳回值也不再是一個Bitmap對象,而是null。

  • 為了避免OOM異常,最好在解析每張圖檔的時候都先檢查一下圖檔的大小,除非你非常信任圖檔的來源,保證這些圖檔都不會超出你程式的可用記憶體。
  • 現在圖檔的大小已經知道了,我們就可以決定是把整張圖檔加載到記憶體中還是加載一個壓縮版的圖檔到記憶體中。以下幾個因素是我們需要考慮的:
    • 預估一下加載整張圖檔所需占用的記憶體。
    • 為了加載這一張圖檔你所願意提供多少記憶體。
    • 用于展示這張圖檔的控件的實際大小。
    • 目前裝置的螢幕尺寸和分辨率。
  • 比如,你的ImageView隻有128x96像素的大小,隻是為了顯示一張縮略圖,這時候把一張2048x1536像素的圖檔完全加載到記憶體中顯然是不值得的。

6.1 對圖檔進行壓縮

  • 怎樣才能對圖檔進行壓縮呢?
    • 通過設定BitmapFactory.Options中inSampleSize的值就可以實作。
  • 比如我們有一張2048x1536像素的圖檔,将inSampleSize的值設定為4,就可以把這張圖檔壓縮成512x384像素。
    • 原本加載這張圖檔需要占用13M的記憶體,壓縮後就隻需要占用0.75M了(假設圖檔是ARGB_8888類型,即每個像素點占用4個位元組)。
  • 下面的方法可以根據傳入的寬和高,計算出合适的inSampleSize值:
    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 heightRatio = Math.round((float) height / (float) reqHeight); 
            final int widthRatio = Math.round((float) width / (float) reqWidth); 
            // 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖檔的寬和高 
            // 一定都會大于等于目标的寬和高。 
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; 
        } 
        return inSampleSize; 
    }             

6.2 設定BitmapFactory.Options屬性

  • 大概步驟如下所示
    • 要将BitmapFactory.Options的inJustDecodeBounds屬性設定為true,解析一次圖檔。注意這個地方是核心,這個解析圖檔并沒有生成bitmap對象(也就是說沒有為它配置設定記憶體控件),而僅僅是拿到它的寬高等屬性。
    • 然後将BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。這一步會壓縮圖檔。
    • 之後再解析一次圖檔,使用新擷取到的inSampleSize值,并把inJustDecodeBounds設定為false,就可以得到壓縮後的圖檔了。此時才正式建立了bitmap對象,由于前面已經對它壓縮了,是以你會發現此時所占記憶體大小已經很少了。
  • 具體的實作代碼
    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, 
            int reqWidth, int reqHeight) { 
        // 第一次解析将inJustDecodeBounds設定為true,來擷取圖檔大小 
        final BitmapFactory.Options options = new BitmapFactory.Options(); 
        options.inJustDecodeBounds = true; 
        BitmapFactory.decodeResource(res, resId, options); 
        // 調用上面定義的方法計算inSampleSize值 
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 
        // 使用擷取到的inSampleSize值再次解析圖檔 
        options.inJustDecodeBounds = false; 
        return BitmapFactory.decodeResource(res, resId, options); 
    }           
  • 思考:inJustDecodeBounds這個參數是幹什麼的?
    • 如果設定為true則表示decode函數不會生成bitmap對象,僅是将圖像相關的參數填充到option對象裡,這樣我們就可以在不生成bitmap而擷取到圖像的相關參數了。
  • 為何設定兩次inJustDecodeBounds屬性?
    • 第一次:設定為true則表示decode函數不會生成bitmap對象,僅是将圖像相關的參數填充到option對象裡,這樣我們就可以在不生成bitmap而擷取到圖像的相關參數。
    • 第二次:将inJustDecodeBounds設定為false再次調用decode函數時就能生成bitmap了。而此時的bitmap已經壓縮減小很多了,是以加載到記憶體中并不會導緻OOM。

6.3 設定bitmap到View上

  • 将任意一張圖檔壓縮成100*100的縮略圖,并在ImageView上展示。
    mImageView.setImageBitmap( 
        decodeSampledBitmapFromResource(getResources(), R.id.ycimage, 100, 100));            

其他介紹

01.關于部落格彙總連結

02.關于我的部落格

項目案例: https://github.com/yangchong211/YCVideoPlayer

繼續閱讀