Android學習之Bitmap個版本詳解

原博客地址:http://blog.csdn.net/xiaanming/article/details/41084843

我們知道Android系統分配給每個應用程序的內存是有限的,Bitmap作爲消耗內存大戶,我們對Bitmap的管理稍有不當就可能引發OutOfMemoryError,而Bitmap對象在不同的Android版本中存在一些差異,今天就給大家介紹下這些差異,並提供一些在使用Bitmap的需要注意的地方。

在Android2.3.3(API 10)及之前的版本中,Bitmap對象與其像素數據是分開存儲的,Bitmap對象存儲在Dalvik heap中,而Bitmap對象的像素數據則存儲在Native Memory(本地內存)中或者說Derict Memory(直接內存)中,這使得存儲在Native Memory中的像素數據的釋放是不可預知的,我們可以調用recycle()方法來對Native Memory中的像素數據進行釋放,前提是你可以清楚的確定Bitmap已不再使用了,如果你調用了Bitmap對象recycle()之後再將Bitmap繪製出來,就會出現"Canvas: trying to use a recycled bitmap"錯誤,而在Android3.0(API 11)之後,Bitmap的像素數據和Bitmap對象一起存儲在Dalvik heap中,所以我們不用手動調用recycle()來釋放Bitmap對象,內存的釋放都交給垃圾回收器來做,也許你會問,爲什麼我在顯示Bitmap對象的時候還是會出現OutOfMemoryError呢?

在說這個問題之前我順便提一下,在Android2.2(API 8)之前,使用的是Serial垃圾收集器,從名字可以看出這是一個單線程的收集器,這裏的”單線程"的意思並不僅僅是使用一個CPU或者一條收集線程去收集垃圾,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,Android2.3之後,這種收集器就被代替了,使用的是併發的垃圾收集器,這意味着我們的垃圾收集線程和我們的工作線程互不影響。

簡單的瞭解垃圾收集器之後,我們對上面的問題舉一個簡單的例子,假如系統啓動了垃圾回收線程去收集垃圾,而此時我們一下子產生大量的Bitmap對象,此時是有可能會產生OutOfMemoryError,因爲垃圾回收器首先要判斷某個對象是否還存活(JAVA語言判斷對象是否存活使用的是根搜索算法 GC Root Tracing),然後利用垃圾回收算法來對垃圾進行回收,不同的垃圾回收器具有不同的回收算法,這些都是需要時間的, 發生OutOfMemoryError的時候,我們要明確到底是因爲內存泄露(Memory Leak)引發的還是內存溢出(Memory overflow)引發的,如果是內存泄露我們需要利用工具(比如MAT)查明內存泄露的代碼並進行改正,如果不存在泄露,換句話來說就是內存中的對象確實還必須活着,那我們可以看看是否可以通過某種途徑,減少對象對內存的消耗,比如我們在使用Bitmap的時候,應該根據View的大小利用BitmapFactory.Options計算合適的inSimpleSize來對Bitmap進行相對應的裁剪,以減少Bitmap對內存的使用,如果上面都做好了還是存在OutOfMemoryError(一般這種情況很少發生)的話,那我們只能調大Dalvik heap的大小了,在Android 3.1以及更高的版本中,我們可以在AndroidManifest.xml的application標籤中增加一個值等於“true”的android:largeHeap屬性來通知Dalvik虛擬機應用程序需要使用較大的Java Heap,但是我們也不鼓勵這麼做。


在Android 2.3及以下管理Bitmap

從上面我們知道,在Android2.3及以下我們推薦使用recycle()方法來釋放內存,我們在使用ListView或者GridView的時候,該在什麼時候去調用recycle()呢?這裏我們用到引用計數,使用一個變量(dispalyRefCount)來記錄Bitmap顯示情況,如果Bitmap繪製在View上面displayRefCount加一, 否則就減一, 只有在displayResCount爲0且Bitmap不爲空且Bitmap沒有調用過recycle()的時候,我們才需求對該Bitmap對象進行recycle(),所以我們需要用一個類來包裝下Bitmap對象,代碼如下

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.example.bitmap;  
  2.   
  3. import android.content.res.Resources;  
  4. import android.graphics.Bitmap;  
  5. import android.graphics.drawable.BitmapDrawable;  
  6.   
  7. public class RecycleBitmapDrawable extends BitmapDrawable {  
  8.     private int displayResCount = 0;  
  9.     private boolean mHasBeenDisplayed;  
  10.   
  11.     public RecycleBitmapDrawable(Resources res, Bitmap bitmap) {  
  12.         super(res, bitmap);  
  13.     }  
  14.       
  15.       
  16.     /** 
  17.      * @param isDisplay 
  18.      */  
  19.     public void setIsDisplayed(boolean isDisplay){  
  20.         synchronized (this) {  
  21.             if(isDisplay){  
  22.                 mHasBeenDisplayed = true;  
  23.                 displayResCount ++;  
  24.             }else{  
  25.                 displayResCount --;  
  26.             }  
  27.         }  
  28.           
  29.         checkState();  
  30.           
  31.     }  
  32.       
  33.     /** 
  34.      * 檢查圖片的一些狀態,判斷是否需要調用recycle 
  35.      */  
  36.     private synchronized void checkState() {  
  37.         if (displayResCount <= 0 && mHasBeenDisplayed  
  38.                 && hasValidBitmap()) {  
  39.             getBitmap().recycle();  
  40.         }  
  41.     }  
  42.       
  43.       
  44.     /** 
  45.      * 判斷Bitmap是否爲空且是否調用過recycle() 
  46.      * @return 
  47.      */  
  48.     private synchronized boolean hasValidBitmap() {  
  49.         Bitmap bitmap = getBitmap();  
  50.         return bitmap != null && !bitmap.isRecycled();  
  51.     }  
  52.   
  53. }  
除了上面這個RecycleBitmapDrawable之外呢,我們還需要一個自定義的ImageView來控制什麼時候顯示Bitmap以及什麼時候隱藏Bitmap對象

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.example.bitmap;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.drawable.Drawable;  
  5. import android.graphics.drawable.LayerDrawable;  
  6. import android.util.AttributeSet;  
  7. import android.widget.ImageView;  
  8.   
  9. public class RecycleImageView extends ImageView {  
  10.     public RecycleImageView(Context context) {  
  11.         super(context);  
  12.     }  
  13.   
  14.     public RecycleImageView(Context context, AttributeSet attrs) {  
  15.         super(context, attrs);  
  16.     }  
  17.   
  18.     public RecycleImageView(Context context, AttributeSet attrs, int defStyle) {  
  19.         super(context, attrs, defStyle);  
  20.     }  
  21.   
  22.     @Override  
  23.     public void setImageDrawable(Drawable drawable) {  
  24.         Drawable previousDrawable = getDrawable();  
  25.         super.setImageDrawable(drawable);  
  26.           
  27.         //顯示新的drawable  
  28.         notifyDrawable(drawable, true);  
  29.   
  30.         //回收之前的圖片  
  31.         notifyDrawable(previousDrawable, false);  
  32.     }  
  33.   
  34.     @Override  
  35.     protected void onDetachedFromWindow() {  
  36.         //當View從窗口脫離的時候,清除drawable  
  37.         setImageDrawable(null);  
  38.   
  39.         super.onDetachedFromWindow();  
  40.     }  
  41.   
  42.     /** 
  43.      * 通知該drawable顯示或者隱藏 
  44.      *  
  45.      * @param drawable 
  46.      * @param isDisplayed 
  47.      */  
  48.     public static void notifyDrawable(Drawable drawable, boolean isDisplayed) {  
  49.         if (drawable instanceof RecycleBitmapDrawable) {  
  50.             ((RecycleBitmapDrawable) drawable).setIsDisplayed(isDisplayed);  
  51.         } else if (drawable instanceof LayerDrawable) {  
  52.             LayerDrawable layerDrawable = (LayerDrawable) drawable;  
  53.             for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {  
  54.                 notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);  
  55.             }  
  56.         }  
  57.     }  
  58.   
  59. }  
這個自定類也比較簡單,重寫了setImageDrawable()方法,在這個方法中我們先獲取ImageView上面的圖片,然後通知之前顯示在ImageView的Drawable不在顯示了,Drawable會判斷是否需要調用recycle(),當View從Window脫離的時候會回調onDetachedFromWindow(),我們在這個方法中回收顯示在ImageView的圖片,具體的使用方法

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. ImageView imageView = new ImageView(context);  
  2.         imageView.setImageDrawable(new RecycleBitmapDrawable(context.getResource(), bitmap));  
只需要用RecycleBitmapDrawable包裝Bitmap對象,然後設置到ImageView上面就可以啦,具體的內存釋放我們不需要管,是不是很方便呢?這是在Android2.3以及以下的版本管理Bitmap的內存。


在Android 3.0及以上管理Bitmap

由於在Android3.0及以上的版本中,Bitmap的像素數據也存儲在Dalvik heap中,所以內存的管理就直接交給垃圾回收器了,我們並不需要手動的去釋放內存,而今天講的主要是BitmapFactory.Options.inBitmap的這個字段,假如這個字段被設置了,我們在解碼Bitmap的時候,他會去重用inBitmap設置的Bitmap,減少內存的分配和釋放,提高了應用的性能,然而在Android 4.4之前,BitmapFactory.Options.inBitmap設置的Bitmap必須和我們需要解碼的Bitmap的大小一致才行,在Android4.4以後,BitmapFactory.Options.inBitmap設置的Bitmap大於或者等於我們需要解碼的Bitmap的大小就OK了,我們先假設一個場景,還是在使用ListView,GridView去加載大量的圖片,爲了提高應用的效率,我們通常會做相對應的內存緩存和硬盤緩存,這裏我們只說內存緩存,而內存緩存官方推薦使用LruCache, 注意LruCache只是起到緩存數據作用,並沒有回收內存。一般我們的代碼會這麼寫

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. package com.example.bitmap;  
  2.   
  3. import java.lang.ref.SoftReference;  
  4. import java.util.Collections;  
  5. import java.util.HashSet;  
  6. import java.util.Iterator;  
  7. import java.util.Set;  
  8.   
  9. import android.annotation.TargetApi;  
  10. import android.graphics.Bitmap;  
  11. import android.graphics.Bitmap.Config;  
  12. import android.graphics.BitmapFactory;  
  13. import android.graphics.drawable.BitmapDrawable;  
  14. import android.os.Build;  
  15. import android.os.Build.VERSION_CODES;  
  16. import android.support.v4.util.LruCache;  
  17.   
  18. public class ImageCache {  
  19.     private final static int MAX_MEMORY = 4 * 102 * 1024;  
  20.     private LruCache<String, BitmapDrawable> mMemoryCache;  
  21.   
  22.     private Set<SoftReference<Bitmap>> mReusableBitmaps;  
  23.   
  24.     private void init() {  
  25.         if (hasHoneycomb()) {  
  26.             mReusableBitmaps = Collections  
  27.                     .synchronizedSet(new HashSet<SoftReference<Bitmap>>());  
  28.         }  
  29.   
  30.         mMemoryCache = new LruCache<String, BitmapDrawable>(MAX_MEMORY) {  
  31.   
  32.             /** 
  33.              * 當保存的BitmapDrawable對象從LruCache中移除出來的時候回調的方法 
  34.              */  
  35.             @Override  
  36.             protected void entryRemoved(boolean evicted, String key,  
  37.                     BitmapDrawable oldValue, BitmapDrawable newValue) {  
  38.   
  39.                 if (hasHoneycomb()) {  
  40.                     mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue  
  41.                             .getBitmap()));  
  42.                 }  
  43.             }  
  44.   
  45.         };  
  46.     }  
  47.   
  48.       
  49.     /** 
  50.      * 從mReusableBitmaps中獲取滿足 能設置到BitmapFactory.Options.inBitmap上面的Bitmap對象 
  51.      * @param options 
  52.      * @return 
  53.      */  
  54.     protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {  
  55.         Bitmap bitmap = null;  
  56.   
  57.         if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {  
  58.             synchronized (mReusableBitmaps) {  
  59.                 final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps  
  60.                         .iterator();  
  61.                 Bitmap item;  
  62.   
  63.                 while (iterator.hasNext()) {  
  64.                     item = iterator.next().get();  
  65.   
  66.                     if (null != item && item.isMutable()) {  
  67.                         if (canUseForInBitmap(item, options)) {  
  68.                             bitmap = item;  
  69.                             iterator.remove();  
  70.                             break;  
  71.                         }  
  72.                     } else {  
  73.                         iterator.remove();  
  74.                     }  
  75.                 }  
  76.             }  
  77.         }  
  78.         return bitmap;  
  79.     }  
  80.   
  81.     /** 
  82.      * 判斷該Bitmap是否可以設置到BitmapFactory.Options.inBitmap上 
  83.      *  
  84.      * @param candidate 
  85.      * @param targetOptions 
  86.      * @return 
  87.      */  
  88.     @TargetApi(VERSION_CODES.KITKAT)  
  89.     public static boolean canUseForInBitmap(Bitmap candidate,  
  90.             BitmapFactory.Options targetOptions) {  
  91.   
  92.         // 在Anroid4.4以後,如果要使用inBitmap的話,只需要解碼的Bitmap比inBitmap設置的小就行了,對inSampleSize  
  93.         // 沒有限制  
  94.         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {  
  95.             int width = targetOptions.outWidth / targetOptions.inSampleSize;  
  96.             int height = targetOptions.outHeight / targetOptions.inSampleSize;  
  97.             int byteCount = width * height  
  98.                     * getBytesPerPixel(candidate.getConfig());  
  99.             return byteCount <= candidate.getAllocationByteCount();  
  100.         }  
  101.   
  102.         // 在Android  
  103.         // 4.4之前,如果想使用inBitmap的話,解碼的Bitmap必須和inBitmap設置的寬高相等,且inSampleSize爲1  
  104.         return candidate.getWidth() == targetOptions.outWidth  
  105.                 && candidate.getHeight() == targetOptions.outHeight  
  106.                 && targetOptions.inSampleSize == 1;  
  107.     }  
  108.   
  109.     /** 
  110.      * 獲取每個像素所佔用的Byte數 
  111.      *  
  112.      * @param config 
  113.      * @return 
  114.      */  
  115.     public static int getBytesPerPixel(Config config) {  
  116.         if (config == Config.ARGB_8888) {  
  117.             return 4;  
  118.         } else if (config == Config.RGB_565) {  
  119.             return 2;  
  120.         } else if (config == Config.ARGB_4444) {  
  121.             return 2;  
  122.         } else if (config == Config.ALPHA_8) {  
  123.             return 1;  
  124.         }  
  125.         return 1;  
  126.     }  
  127.   
  128.     @TargetApi(VERSION_CODES.HONEYCOMB)  
  129.     public static boolean hasHoneycomb() {  
  130.         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;  
  131.     }  
  132.   
  133. }  
上面只是一些事例性的代碼,將從LruCache中移除的BitmapDrawable對象的弱引用保存在一個set中,然後從set中獲取滿足BitmapFactory.Options.inBitmap條件的Bitmap對象用來提高解碼Bitmap性能,使用如下

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public static Bitmap decodeSampledBitmapFromFile(String filename,  
  2.             int reqWidth, int reqHeight) {  
  3.   
  4.         final BitmapFactory.Options options = new BitmapFactory.Options();  
  5.         ...  
  6.         BitmapFactory.decodeFile(filename, options);  
  7.         ...  
  8.   
  9.         // If we're running on Honeycomb or newer, try to use inBitmap.  
  10.         if (ImageCache.hasHoneycomb()) {  
  11.              options.inMutable = true;  
  12.   
  13.                 if (cache != null) {  
  14.                     Bitmap inBitmap = cache.getBitmapFromReusableSet(options);  
  15.   
  16.                     if (inBitmap != null) {  
  17.                         options.inBitmap = inBitmap;  
  18.                     }  
  19.                 }  
  20.         }  
  21.         ...  
  22.         return BitmapFactory.decodeFile(filename, options);  
  23.     }  

通過這篇文章你是不是對Bitmap對象有了更進一步的瞭解,在應用加載大量的Bitmap對象的時候,如果你做到上面幾點,我相信應用發生OutOfMemoryError的概率會很小,並且性能會得到一定的提升,我經常會看到一些同學在評價一個圖片加載框架好不好的時候,比較片面的以自己使用過程中是否發生OutOfMemoryError來定論,當然經常性的發生OutOfMemoryError你應該先檢查你的代碼是否存在問題,一般一些比較成熟的框架是不存在很嚴重的問題,畢竟它也經過很多的考驗才被人熟知的,今天的講解就到這裏了,有疑問的同學可以在下面留言!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章