1. 程式人生 > >Android 有效的展示大圖片(四)

Android 有效的展示大圖片(四)

下面是Androidbitmap的記憶體管理的進化過程:

Android2.2之前的版本中,當垃圾回收執行緒開始時,你的app的執行緒就會掛起。這就會導致使用者體驗降級。Android2.3之後使得垃圾回收機制可以併發執行。這也就意味著當一個bitmap沒有指向自己的引用時,可以被垃圾回收機制回收,但是當別的地方申請引用記憶體時。這一塊記憶體也可以被生宣告,而並非要等到整個垃圾回收執行緒結束

而在Android 2.3.3(API級別10)和之前的版本中,支援一個bitmap的畫素的資料是儲存在native heap中記憶體。和儲存在Dalvik堆的bitmap是分開存的。(可以簡單的理解為一部分存在c中,一部分存在Java中。)我們並不知道儲存在c 中的那些畫素資料什麼時候被釋放,這樣就可能導致

app超出記憶體限制而崩潰。而Android3.0的版本則開始把畫素資料和bitmap一起放在了Dalvik中(即java中,這樣就方便我們用垃圾回收機制來管理)。

下面的部分則是講解對於不同的版本,怎樣優化bitmap的記憶體管理

Android2.3.3以及之前的版本上的記憶體管理


2.3.3以及之前,推薦用recycle()方法(呼叫底層c的方法將圖片釋放)。如果你的app中有大量圖片,那麼你就有可能報錯oom.recycle()方法則允許app可以儘快的將不用的記憶體重新宣告以加以利用。

注意:只有你在確定該bitmap沒有用時才能呼叫

recycle()方法。如果你回收了這張圖而你在後面利用這張圖,那麼就會報錯: "Canvas:trying to use a recycled bitmap".

接下來的程式碼段(上節非同步載入圖片程式原始碼中可找到該程式碼段)就是一個利用recycle()的例子。見原始碼中RecyclingBitmapDrawable.java檔案。它利用兩個變數來追蹤一個圖片是否顯示或者是還是否有必要呆在記憶體中。

當下面的條件都被滿足時那麼該圖片就要回收:

·        兩個計數變數都是零.

·        圖片不為空而且還沒有被回收.

•	private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
•	//通知該drawable(此處為bitmap形式)展示狀態(已經展示或未展示)已經改變
•	//用一個變數mDisplayRefCount來標記展示次數。
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {//被展示,通常是被載入在了imageview上
            mDisplayRefCount++;
            mHasBeenDisplayed = true;
        } else {//不被展示,通常是被新的bitmap覆蓋。
            mDisplayRefCount--;
        }
    }
    //檢查是否有回收的bitmap
checkState();
}

//通知該drawable(此處為bitmap形式)展示狀態(已經展示或未展示)已經改變
//用一個變數mDisplayRefCount來標記展示次數。
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {//被加入快取,通常是後臺下載圖片後存入快取
            mCacheRefCount++;
        } else {//移出快取,比如在儲存空間到達上限時移出
            mCacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

private synchronized void checkState() {
    //如果一個bitmap快取和展示的次數為零,即曾經展示過,如今要被移出記憶體而且     //無需再展示了那麼這個bitmap就可以回收了。
       if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();//    }
}

private synchronized boolean hasValidBitmap() {
    Bitmap bitmap = getBitmap();
    return bitmap != null && !bitmap.isRecycled();
}

Android3.0以及之後的版本上的記憶體管理

3.0以上,多了一個變數: BitmapFactory.Options.inBitmap。如果這個option被設定了inBitmap,那麼配置了這個option的解碼方法就會在下載畫素的時候重複利用已存在的bitmap。這就是說那個bitmap所佔的記憶體被重新利用。這樣就會使得程式表現更好。

但是,在怎樣使用inBitmap方面,有一定的限制條件。尤其是在4.4之前。只有同等大小的bitmap才可以使用。

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

// If you're running on Honeycomb or newer, create
// a HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {//3.0以上
    mReusableBitmaps = 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)) {//2.3.3之前
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

Use an existing bitmap

在程式執行中,decode方法會尋找有沒有已經存在的bitmap.比如:

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

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

    // If we're running on Honeycomb or newer, try to use inBitmap.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

下面程式碼段中的addInBitmapOptions()就是上面提到的。這個方法尋找一個已經存在的bitmap作為inBitmap配置option。必須注意只有當這個方法找到合適的匹配者時才會給optioninbitmap賦值,這就提醒我們要注意為空的情形。

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) {
            // If a suitable bitmap has been found, set it as the value of
            // inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

// This method iterates through the reusable bitmaps, looking for one 
// to use for inBitmap:
//這個方法遍歷reusable bitmaps以尋找一個可以作為inBitmap值的bitmap
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;

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

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

            if (null != item && item.isMutable()) {
                // Check to see it the item can be used for inBitmap.
                if (canUseForInBitmap(item, options)) {
                    bitmap = item;

                    // Remove from reusable set so it can't be used again.
                    iterator.remove();
                    break;
                }
            } else {
                // Remove from the set if the reference has been cleared.
                iterator.remove();
            }
        }
    }
    return bitmap;
}

最終是這個方法來決定是否每個待用的bitmap滿足inBitmapsize標準

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

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
       //如果在4.4以上的平臺中,只要是新的點陣圖比準備reuse的點陣圖的位元組少,那麼這個代用的準備reuse的點陣圖就可以重新利用。
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

  
//在早期的版本中,這個尺寸大小必須嚴格一致,而且inSamplesize必須是1才可以
    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;
}

總結:在3.0之前當超出記憶體時的處理是直接在記憶體中recycle掉,而在3.0之後則是先放在hashset中,如果hashset中存的bitmap不能被之後的解析圖片所利用時那麼就會被remove掉,同樣會被回收。