天天看点

管理 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 也有限制(不确定从哪个版本开始)

继续阅读