高效地加載圖片(四) 管理緩存

 

除了緩存圖片意外,還有一些其餘的方式來促進GC的效率和圖片的複用.不一樣的Android系統版本有不一樣的處理策略.BitmapFun中就包含了這個類,可以使咱們高效地構建咱們的項目.html

爲了開始如下教程,咱們須要先介紹一下Android系統對Bitmap管理的進化史.java

  • 在Android2.2(API level 8)以及更低的版本中,當垃圾被回收時,應用的線程會被中止,這會形成必定程度的延時.在Android 2.3中,加入了併發回收機制,這意味着當Bitmap再也不被使用的時候,內存會被很快地回收.
  • 在Android 2.3.3(API level 10)以及更低版本中,像素數據是被存儲在本地內存中的,而且和Bitmap自己是分離的,Bitmap存儲在Dalvik堆中.本地內存中的像素數據並不以一種可預知的方式釋放,可能會形成應用內存溢出和崩潰.而在Android 3.0(API level 11)中,像素數據和Bitmap一塊兒被保存在Dalvik堆中.

如下內容描述瞭如何爲不一樣的Android版本選擇最佳的圖片緩存管理方式.android

在Android 2.3.3以及更低版本中管理緩存緩存

在Android 2.3.3(API level 10)以及更低版本中,建議使用Bitmap.recycle()方法.若是你正在展現大量的圖片,應用可能會內存溢出.這時候recycle()方法可使內存被儘量快地回收.併發

注意:你只能在肯定了Bitmap確實再也不被使用了以後才能調用recycle()方法.若是你調用recycle()方法釋放了Bitmap,而稍後卻又使用這個Bitmap,這時就會出現"Canvas: trying to use a recycled bitmap"錯誤.ide

如下代碼片斷是recycle()方法調用的示例.這裏使用了引用計數器的方式(經過變量mDisplayRefCount和mCacheRefCount)來追蹤一個Bitmap是否還在被使用或者存在於緩存中.當知足以下條件時,此處就會回收Bitmap:ui

  • 引用計數器變量mDisplayRefCount和mCacheRefCount的值都爲0時.
  • Bitmap不爲null,並且沒有被回收.
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// 通知當前Drawable,當前被使用的狀態改變了
// 保持一個計數器,用來決定當前Drawable什麼時候被回收
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
			// 當被顯示時,則計數器mDisplayRefCount的值+1
            mDisplayRefCount++;
			// 標誌當前Drawable被顯示了
            mHasBeenDisplayed = true;
        } else {
			// 當一個引用被釋放時,計數器mDisplayRefCount的值-1
            mDisplayRefCount--;
        }
    }
    // 確認recycle()方法是否已經被調用了
    checkState();
}

// 通知當前Drawable緩存狀態改變了
// 保持一個計數器用於決定什麼時候這個Drawable再也不被緩存
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            mCacheRefCount++;
        } else {
            mCacheRefCount--;
        }
    }
    // 確認recycle()方法是否已經被調用了
    checkState();
}

private synchronized void checkState() {
	// 若是當前Drawable的內存和展現計數器都爲0,並且當前Drawable還可用
	// 則釋放掉它
    if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

// 判斷Drawable對應的Bitmap是否可用
private synchronized boolean hasValidBitmap() {
    Bitmap bitmap = getBitmap();
    return bitmap != null && !bitmap.isRecycled();
}

在Android 3.0以及更高版本中管理緩存this

 

從Android 3.0(API level 11)開始,引入了BitmapFactory.Options.inBitmap字段,若是這個屬性被設置了,擁有這個Options對象的方法在解析圖片的時候會嘗試複用一張已存在的圖片.這意味着圖片緩存被服用了,這意味着更流暢的用戶體驗以及更好的內存分配和回收.然而,要使用inBitmap有這必定的限制.尤爲須要注意的是,在Android 4.4(API level 19)以前,只有相同大小的Btiamp纔會被複用.更詳細的用法,請參見inBitmap的文檔.spa

保存一個Bitmap以備後來使用線程

下面的代碼片斷示範瞭如何保存一個Bitmap以備後來使用.當在Android 3.0以及更高版本平臺中時,一個Bitmap被從LrcCache中移除後,它的軟引用會被保存到一個HashSet中,以備inBitmap後來使用:

Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

// 當程序運行在Honeycomb(Android 3.1)及以上版本的系統中時,建立一個
// 同步的HashSet用於存放可複用的Bitmap的引用
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
			// 若是被移除的Drawable是RecyclingBitmapDrawable,則通知它
			// 已經被從內存緩存中移除
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
			// 若是被移除的Drawable是一個普通的BitmapDrawable
            if (Utils.hasHoneycomb()) {
				// 若是運行在Honeycomb(Android 3.1)及以上系統中,則爲Bitmap添加
				// 到一個保存軟引用的集合中,以備後來被inBitmap使用
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

複用一個已經存在的Bitmap

在解析圖片時,會檢查是否已經存在一個可用的Bitmap,以下:

public static Bitmap decodeSampledBitmapFromFile(String filename,
        int reqWidth, int reqHeight, ImageCache cache) {

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

	// 若是應用運行在Honeycomb及以上系統中,則嘗試使用inBitmap複用圖片
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

如下代碼片斷描述瞭如何查找一個已經存在的Bitmap並將它設置爲inBitmap的值.注意,這個方法只有在找到了符合條件的Bitmap纔會爲inBitmap賦值(咱們不能樂觀地假定圖片會被找到):

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
	// inBitmap僅能接受可編輯的Bitmap,因此此處須要強制解碼器
	// 返回一個可編輯的Bitmap
    options.inMutable = true;

    if (cache != null) {
		// 嘗試查找知足條件的Bitmap給inBitmap賦值
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
			// 若是查找到了符合條件的Bitmap,則賦值給inBitmap
            options.inBitmap = inBitmap;
        }
    }
}

// 這個方法遍歷了保存弱引用的集合,用於查找一個合適的Bitmap
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()) {
					// 檢查當前的Bitmap是否知足inBitmap的複用條件
                    // Check to see it the item can be used for inBitmap.
                    if (canUseForInBitmap(item, options)) {
						// 若是知足複用條件,則給Bitmap賦值,並在方法結束時返回
                        bitmap = item;
						// 在給返回值賦值後,將當前Bitmap的引用從集合中移除
                        iterator.remove();
                        break;
                    }
                } else {
					// 若是讀取到的是空引用,則將該引用移除
                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

最後,下面的這個方法可以檢查哪一個Bitmap知足inBitmap的複用條件:

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
		// 從Android 4.4(KitKat)開始,若是當前須要的Bitmap尺寸(內存中佔用的字節數)比緩存中的Bitmap尺寸小
		// 而且同一張圖片的Bitmap存在,咱們就能夠複用它
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
		// 當前須要的Bitmap佔用字節數
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
		// 若是須要的Bitmap尺寸小於原Bitmap,則返回true
        return byteCount <= candidate.getAllocationByteCount();
    }

	// 在Android 4.4之前,尺寸必須徹底一致,而且inSampleSize爲1時
	// Bitmap才能被複用
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == 1;
}

/**
 * 一個用於判斷在不一樣的參數下,每一個像素佔用字節數的方法
 */
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;
}
相關文章
相關標籤/搜索