1. 程式人生 > >Android 那些你所不知道的Bitmap物件詳解

Android 那些你所不知道的Bitmap物件詳解

我們知道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物件,程式碼如下

package com.example.bitmap;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;

public class RecycleBitmapDrawable extends BitmapDrawable {
	private int displayResCount = 0;
	private boolean mHasBeenDisplayed;

    public RecycleBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }
	
    
    /**
     * @param isDisplay
     */
	public void setIsDisplayed(boolean isDisplay){
		synchronized (this) {
			if(isDisplay){
				mHasBeenDisplayed = true;
				displayResCount ++;
			}else{
				displayResCount --;
			}
		}
		
		checkState();
		
	}
	
	/**
	 * 檢查圖片的一些狀態,判斷是否需要呼叫recycle
	 */
	private synchronized void checkState() {
	    if (displayResCount <= 0 && mHasBeenDisplayed
	            && hasValidBitmap()) {
	        getBitmap().recycle();
	    }
	}
	
	
	/**
	 * 判斷Bitmap是否為空且是否呼叫過recycle()
	 * @return
	 */
	private synchronized boolean hasValidBitmap() {
	    Bitmap bitmap = getBitmap();
	    return bitmap != null && !bitmap.isRecycled();
	}

}
除了上面這個RecycleBitmapDrawable之外呢,我們還需要一個自定義的ImageView來控制什麼時候顯示Bitmap以及什麼時候隱藏Bitmap物件
package com.example.bitmap;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;

public class RecycleImageView extends ImageView {
	public RecycleImageView(Context context) {
		super(context);
	}

	public RecycleImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public RecycleImageView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	@Override
	public void setImageDrawable(Drawable drawable) {
		Drawable previousDrawable = getDrawable();
		super.setImageDrawable(drawable);
		
		//顯示新的drawable
		notifyDrawable(drawable, true);

		//回收之前的圖片
		notifyDrawable(previousDrawable, false);
	}

	@Override
	protected void onDetachedFromWindow() {
		//當View從視窗脫離的時候,清除drawable
		setImageDrawable(null);

		super.onDetachedFromWindow();
	}

	/**
	 * 通知該drawable顯示或者隱藏
	 * 
	 * @param drawable
	 * @param isDisplayed
	 */
	public static void notifyDrawable(Drawable drawable, boolean isDisplayed) {
		if (drawable instanceof RecycleBitmapDrawable) {
			((RecycleBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
		} else if (drawable instanceof LayerDrawable) {
			LayerDrawable layerDrawable = (LayerDrawable) drawable;
			for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
				notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
			}
		}
	}

}
這個自定類也比較簡單,重寫了setImageDrawable()方法,在這個方法中我們先獲取ImageView上面的圖片,然後通知之前顯示在ImageView的Drawable不在顯示了,Drawable會判斷是否需要呼叫recycle(),當View從Window脫離的時候會回撥onDetachedFromWindow(),我們在這個方法中回收顯示在ImageView的圖片,具體的使用方法
ImageView imageView = new ImageView(context);
		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只是起到快取資料作用,並沒有回收記憶體。一般我們的程式碼會這麼寫

package com.example.bitmap;

import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.support.v4.util.LruCache;

public class ImageCache {
	private final static int MAX_MEMORY = 4 * 102 * 1024;
	private LruCache<String, BitmapDrawable> mMemoryCache;

	private Set<SoftReference<Bitmap>> mReusableBitmaps;

	private void init() {
		if (hasHoneycomb()) {
			mReusableBitmaps = Collections
					.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
		}

		mMemoryCache = new LruCache<String, BitmapDrawable>(MAX_MEMORY) {

			/**
			 * 當儲存的BitmapDrawable物件從LruCache中移除出來的時候回撥的方法
			 */
			@Override
			protected void entryRemoved(boolean evicted, String key,
					BitmapDrawable oldValue, BitmapDrawable newValue) {

				if (hasHoneycomb()) {
					mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue
							.getBitmap()));
				}
			}

		};
	}

	
	/**
	 * 從mReusableBitmaps中獲取滿足 能設定到BitmapFactory.Options.inBitmap上面的Bitmap物件
	 * @param options
	 * @return
	 */
	protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
		Bitmap bitmap = null;

		if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
			synchronized (mReusableBitmaps) {
				final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps
						.iterator();
				Bitmap item;

				while (iterator.hasNext()) {
					item = iterator.next().get();

					if (null != item && item.isMutable()) {
						if (canUseForInBitmap(item, options)) {
							bitmap = item;
							iterator.remove();
							break;
						}
					} else {
						iterator.remove();
					}
				}
			}
		}
		return bitmap;
	}

	/**
	 * 判斷該Bitmap是否可以設定到BitmapFactory.Options.inBitmap上
	 * 
	 * @param candidate
	 * @param targetOptions
	 * @return
	 */
	@TargetApi(VERSION_CODES.KITKAT)
	public static boolean canUseForInBitmap(Bitmap candidate,
			BitmapFactory.Options targetOptions) {

		// 在Anroid4.4以後,如果要使用inBitmap的話,只需要解碼的Bitmap比inBitmap設定的小就行了,對inSampleSize
		// 沒有限制
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
			int width = targetOptions.outWidth / targetOptions.inSampleSize;
			int height = targetOptions.outHeight / targetOptions.inSampleSize;
			int byteCount = width * height
					* getBytesPerPixel(candidate.getConfig());
			return byteCount <= candidate.getAllocationByteCount();
		}

		// 在Android
		// 4.4之前,如果想使用inBitmap的話,解碼的Bitmap必須和inBitmap設定的寬高相等,且inSampleSize為1
		return candidate.getWidth() == targetOptions.outWidth
				&& candidate.getHeight() == targetOptions.outHeight
				&& targetOptions.inSampleSize == 1;
	}

	/**
	 * 獲取每個畫素所佔用的Byte數
	 * 
	 * @param config
	 * @return
	 */
	public static int getBytesPerPixel(Config config) {
		if (config == Config.ARGB_8888) {
			return 4;
		} else if (config == Config.RGB_565) {
			return 2;
		} else if (config == Config.ARGB_4444) {
			return 2;
		} else if (config == Config.ALPHA_8) {
			return 1;
		}
		return 1;
	}

	@TargetApi(VERSION_CODES.HONEYCOMB)
	public static boolean hasHoneycomb() {
		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
	}

}
上面只是一些事例性的程式碼,將從LruCache中移除的BitmapDrawable物件的弱引用儲存在一個set中,然後從set中獲取滿足BitmapFactory.Options.inBitmap條件的Bitmap物件用來提高解碼Bitmap效能,使用如下
public static Bitmap decodeSampledBitmapFromFile(String filename,
	        int reqWidth, int reqHeight) {

	    final BitmapFactory.Options options = new BitmapFactory.Options();
	    ...
	    BitmapFactory.decodeFile(filename, options);
	    ...

	    // If we're running on Honeycomb or newer, try to use inBitmap.
	    if (ImageCache.hasHoneycomb()) {
	    	 options.inMutable = true;

	    	    if (cache != null) {
	    	        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

	    	        if (inBitmap != null) {
	    	            options.inBitmap = inBitmap;
	    	        }
	    	    }
	    }
	    ...
	    return BitmapFactory.decodeFile(filename, options);
	}

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