管理 Bitmap 記憶體(譯)

orangex發表於2018-05-18

譯自 :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:

  • mDisplayRefCountmCacheRefCount的引用計數都為0
  • bitmap 不為 null且它仍然未被回收
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// 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 <= 0 && mDisplayRefCount <= 0 && 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 == 1;
}

/**
 * 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 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;
}
複製程式碼

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

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

相關文章