Bitmap與OOM

Bitmap所造成的OOM

圖片是一個很耗內存的資源,因此經常會遇到OOM。比如從本地文件中讀取圖片,然后在GridView中顯示出來,如果不做處理,OOM就極有可能發生。

Bitmap引起OOM的原因:

1.圖片使用完成后,沒有及時的釋放,導致Bitmap占用的內存越來越大,而安卓提供給Bitmap的內存是有一定限制的,當超出該內存時,自然就發生了OOM

2.圖片過大

這里的圖片過大是指加載到內存時所占用的內存,并不是圖片自身的大小。而圖片加載到內存中時所占用的內存是根據圖片的分辨率以及它的配置(ARGB值)計算的。舉個例子:

假如有一張分辨率為2048x1536的圖片,它的配置為ARGB_8888,那么它加載到內存時的大小就是2048x1526x4/1024/1024=12M.,因此當將這張圖片設置到ImageView上時,將會出現OOM(超過了Android分配給Bitmap的上限8M)。

補充:ARGB表示圖片的配置,分表代表:透明度、紅色、綠色和藍色。這幾個參數的值越高代表圖像的質量越好,那么也就越占內存。就拿ARGB_8888來說,A、R、G、B這幾個參數分別占8位,那么總共占32位,代表一個像素點占32位大小即4個字節,那么一個100x100分辨率的圖片就占了100x100x4/1024/1024=0.04M的大小的空間。

高效加載Bitmap

當將一個圖片加載到內存,在UI上呈現時,需要考慮一下幾個因素:

1.預計加載完整張圖片所需要的內存空間

2.呈現這張圖片時控件的大小

3.屏幕大小與屏幕像素密度

如果我們要加載的圖片的分辨率比較大,而呈現它的控件(比如ImageView)比較小,那我們如果直接將這張圖片加載到這個控件上顯然是不合適的,因此我們需要對圖片的分辨率就行壓縮。如何去進行圖片的壓縮呢?

BitmapFactory提供了四種解碼(decode)的方法(decodeByteArray(), decodeFile(), decodeResource(),decodeStream()),每一種方法都可以通過BitmapFactory.Options設置一些附加的標記,以此來指定解碼選項。

Options有一個inJustDecodeBunds屬性,當我們將其設置為true時,表示此時并不加載Bitmap到內存中,而是返回一個null,但是此時我們可以通過options獲取到當前bitmap的寬和高,根據這個寬和高,我們再根據目標寬和高計算出一個合適的采樣率采樣率inSampleSize ,然后將其賦值給Options.inSampleSize屬性,這樣在加載圖片的時候,將會得到一個壓縮的圖片到內存中。以下是示例代碼:

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);

// 根據目標寬和高 以及當前圖片的大小 計算出壓縮比率 
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// 將inJustDecodeBounds設置為false 真正加載圖片 然后根據壓縮比率壓縮圖片 再去解碼
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

//計算壓縮比率 android官方提供的算法
public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
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;

    // Calculate the largest inSampleSize value that is a power of 2 and keeps both
    // height and width larger than the requested height and width.
    while ((halfHeight / inSampleSize) > reqHeight
            && (halfWidth / inSampleSize) > reqWidth) {
        inSampleSize *= 2;
    }
}

return inSampleSize;
}

關于采樣率與圖片分辨率壓縮大小的關系:

1.如果inSample=1則表明與原圖一樣
2.如果inSample=2則表示寬和高均縮小為1/2
3.inSample的值一般為2的冪次方

假如 一個分辨率為2048x1536的圖片,如果設置 inSampleSize 為4,那么會產出一個大約512x384大小的Bitmap。加載這張縮小的圖片僅僅使用大概0.75MB的內存,如果是加載完整尺寸的圖片,那么大概需要花費12MB(前提都是Bitmap的配置是 ARGB_8888.

緩存Bitmap
當需要加載大量的圖片時,圖片的緩存機制就特別重要。因為在移動端,用戶大多都是使用的移動流量,如果每次都從網絡獲取圖片,一是會耗費大量的流量,二是在網絡不佳的時候加載會非常的慢,用戶體驗均不好。因此需要定義一種緩存策略可以應對上述問題。關于圖片的緩存通常有兩種:

1.內存緩存,對應的緩存算法是LruCache<k,v>(近期最少使用算法),Android提供了該算法

LruCache是一個泛型類,它的內部采用一個LinkedHashMap以強引用的方式存儲外界的緩存對象,其提供了get和put方法來完成緩存的獲取和添加操作,當緩存滿時,LruCache會移除較早使用的緩存對象,然后再添加新的緩存對象。

補充:之所以使用LinkedHashMap來實現LruCache是因為LinkedHashMap內部采用了雙向鏈表的方式,它可以以訪問順序進行元素的排序。比如通過get方法獲取了一個元素,那么就將這個元素放到鏈表的尾部,通過不斷的get操作就得到了一個訪問順序的鏈表,這樣位于鏈表頭部的就是較早的元素。因此非常適合于LruCache算法的思想,在緩存滿時,將鏈表頭部的對象移除即可。LruCache經典使用方式:

    //app最大可用內存
    int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024);
    //緩存大小
    int cacheSize = maxMemory/8;
    mMemoryCache = new LruCache<String,Bitmap>(cacheSize) {
        //計算緩存對象的大小
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getRowBytes()*value.getHeight()/1024;
        }
    };
    
    //獲取緩存對象
    mMemoryCache.get(key);
    //添加緩存對象
    mMemoryCache.put(key,bitmap);

2.磁盤緩存,對應的緩存算法是DiskLruCache,雖然不是官方提供的,但得到官方的認可。可以通過下面的鏈接進行源碼下載:

http://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

DiskLruCache的創建

public static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)  

DiskLruCache不能通過構造方法來創建,而是用open方法,它有四個參數:
1.directory:表示磁盤緩存的文件路徑可以選擇Sd卡上的緩存目錄:/sdcard/Android/data/package_name/cache,也可以選擇其他的目錄作為緩存目錄,如果希望保留的緩存數據在app卸載時,也刪除,那么應該選擇sd卡上的緩存目錄,否則的話選擇其他的目錄。

2.appVersion:app版本號,初始設為1.該參數表示當版本發生變化時,DiskLruCache會清空之前的緩存文件

3.valueCount 表示同一個key可以對應多少個文件,一般為1

4.maxSize 緩存的總大小

具體的使用參考這個博客: http://blog.csdn.net/guolin_blog/article/details/28863651

使用Bitmap的一些優化方法

  1. 對圖片采用軟引用,調用recycle,及時的回收Bitmap所占用的內存。比如:View如果使用了bitmap,就應該在這個View不再繪制了的時候回收;如果Activity使用了bitmap,就可以在onStop或者onDestroy方法中回收。

    SoftReference<Bitmap> bitmap;
    bitmap = new SoftReference<Bitmap>(pBitmap);
       if(bitmap != null){  
       if(bitmap.get() != null && !bitmap.get().isRecycled()){ 
           bitmap.get().recycle(); 
           bitmap = null;  
       } 
   }

2.對高分辨率圖片進行壓縮,詳情參見高效加載Bitmap部分

3.關于ListView和GridView加載大量圖片時的優化:

 3.1. 不要在getView方法中執行耗時操作,比如加載Bitmap,應將加載動作放到一個異步任務中,比如AsyncTask
 3.2. 在快速滑動列表的時候,停止加載Bitmap,當用戶停止滑動時再去加載。因為當用戶快速上下滑動時,如果去加載Bitmap的話可能會產生大量的異步任務,會造成線程池的擁堵以及大量的更新UI操作,因此會造成卡頓。
 3.3 對當前的Activity開啟硬件加速。
 3.4 為防止因異步下載圖片而造成錯位問題,對ImageView設置Tag,將圖片的Url作為tag的標記,當設置圖片時,去判斷當前ImageView的tag是否等于當前的圖片的url,如果相當則顯示否則的話不予加載。

參考鏈接: http://hukai.me/android-training-course-in-chinese/graphics/displaying-bitmaps/load-bitmap.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容