天天看點

管理 Bitmap 記憶體(譯)

譯自 :Managing Bitmap Memory

作為對 Caching Bitmaps 中描述的步驟的補充,你可以做很多事來促進 GC 和 Bitmap 的複用。而推薦的政策則取決于你面向的 Android 版本。BitmapFun 樣例 app 向你展示如何設計才能讓你的 app 在割裂的 Android 版本中高效運轉。

在課程開始之前你得掌握一些基礎,看看安卓中 Bitmap 記憶體管理的演化吧。

  • 在 Android 2.2(API level 8)及以下的版本中,GC 發生的時候,你的 APP 的所有線程會停止。這會導緻延遲進而降低性能。Android 2.3 中加入了并發垃圾收集機制,這意味着在 bitmap 不再被引用後,記憶體會被迅速回收。
  • 在 Android 2.3.3(API level 10)及以下,bitmap 的 backing pixel data[^1](像素資料)被存儲在 native 記憶體中,與存儲在 Dalvik 堆中的 bitmap 本身是分開的。native 記憶體中的 pixel data 并非以一種可預見的方式被釋放,可能會導緻應用瞬間記憶體超出限制[^2]然後崩潰。從 Android 3.0(API level 11)到 Android 7.1(API level 25),pixel data 和與其關聯的 bitmap 一起被存儲于 Dalvik heap 中。==在 Android 8.0(API level 26)和更高版本中,bitmap 的 pixel data(又被重新)存儲在 native heap 中。==

以下部分描述了在不同版本中如何優化 bitmap 記憶體管理

在 Android 2.3.3 及以下的版本中管理記憶體

在 Android 2.3.3(API level 10)及以下,建議使用

recycle()

。如果你在 App 中展示大量的位圖,你很有可能遇到 OOM errors。

recycle()

方法讓 App 盡可能快的複用記憶體。

注意:你隻有在确定了 bitmap 不再被使用的時候才應該使用

recycle()

。如果你調用了

recycle()

然後嘗試繪制 bitmap,就會報錯:“Canvas: trying to use a recycled bitmap”

接下來的代碼片段提供了一個調用

recycle()

的例子。它使用引用計數(對應變量

mDisplayRefCount

and

mCacheRefCount

)來追蹤 bitmap 是否正在被展示或者存在于緩存中。這段代碼在這些條件被滿足的時候會去回收 bitmap:

  • mDisplayRefCount

    mCacheRefCount

    的引用計數都為0
  • bitmap 不為

    null

    且它仍然未被回收
private int mCacheRefCount = ;
private int mDisplayRefCount = ;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
            mDisplayRefCount++;
            mHasBeenDisplayed = true;
        } else {
            mDisplayRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
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() {
    // If the drawable cache and display ref counts = 0, and this drawable
    // has been displayed, then recycle.
    if (mCacheRefCount <=  && mDisplayRefCount <=  && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

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

在 Android 3.0 及以上的版本中管理記憶體

Android 3.0(API level 11)引入了

BtimapFactory.Options.inBitmap

字段。如果設定了這個選項,接受

Options

對象(作為參數)的解碼方法會在加載内容時嘗試複用已存在的 bitmap。這意味着 bitmap 的記憶體被複用了,進而提高了性能并不再需要配置設定記憶體和釋放記憶體(這樣的麻煩事了)。然而,在如何使用

inBitmap

上存在一定的限制。特别是在 Android 4.4 (API level 19)之前,隻支援等大小 bitmap(的複用)。詳見

inBitmap

文檔

儲存 bitmap 供之後使用

接下來的片段示範了在樣例 App 中一個已存在的 bitmap 是如何存儲以供不時之需的。當 App 運作于 Android 3.0 或更高版本并且一個 bitmap 從 LruCache 中被剔除時,一個指向該 bitmap 的軟引用被放進了一個 HashSet 中,以供之後可能因為 inBitmap 而需要的重用。

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

// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
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)) {
            // 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()));
            }
        }
    }
....
}
複制代碼
           

####使用已存在的 Bitmap

在運作着的 app 中,解碼方法會去檢查是否有它可以使用的已存在的 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 (字段)。注意這個方法隻會在尋找到合适的比對項時才指派給 inBitmap (也就是說你的代碼決不該假定一定能比對到):

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for 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:
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()) {
                    // 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;
}
複制代碼
           

最終,這個方法會根據是否滿足大小上的限制決定一個候選位圖是否能供 inBitmap 使用:

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

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == ;
}

/**
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
 */
static int getBytesPerPixel(Config config) {
    if (config == Config.ARGB_8888) {
        return ;
    } else if (config == Config.RGB_565) {
        return ;
    } else if (config == Config.ARGB_4444) {
        return ;
    } else if (config == Config.ALPHA_8) {
        return ;
    }
    return ;
}
複制代碼
           

[^1]: 也就是 bitmap 的成員 mBuffer 所存儲的東西, bitmap 記憶體占用幾乎全部集中在它身上

[^2]: 這裡指的是 native 記憶體,而不是我們通常說的 OOM , Android 對 每個應用(程序)的 native heap 也有限制(不确定從哪個版本開始)

繼續閱讀