Android官方开发文档Training系列课程中文版:高效显示位图之管理位图内存

原文地址:http://developer.android.com/training/displaying-bitmaps/manage-memory.html

除了在上一节中描述的步骤之外,还有一些细节上的事情可以促进垃圾回收器的回收及位图的复用。其推荐的策略取决于Android的目标版本。示例APP BitmapFun展示了如何使应用程序在不同的版本上高效的工作。

为了给这节课的知识奠定一些基础,下面有一些Android系统如何管理位图内存的一些改进需要了解:

  • 在Android 2.2之前,当垃圾回收器回收时,应用的线程会被停止。这会降低性能发生延迟。Android 2.3增加了并发收集垃圾的功能,这意味着内存回收不久之后位图不可再被引用。
  • 在Android 2.3.3之前,位图对应的支撑数据被存放在本地内存中。这与位图本身是分离的,它被存储在Dalvik堆栈之中。在意料之中位于本地内存中的像素数据是不会被释放的,可能会导致程序超过自身的内存限制然后崩溃。从在Android 3.0开始,像素数据与之相关的位图被一同存入了Dalvik虚拟机堆栈内。

下面的部分会介绍如何对不同的安卓版本进行位图内存的优化与管理。

在Android 2.3之前的版本上管理内存

在2.3之前推荐使用recycle()方法。如果你在APP内展示了大量的位图数据,那么你很有可能会遇到OutOfMemoryError错误。recycle()方法允许应用尽可能的回收内存。

Caution: 你应该在确保位图不再使用的时候使用recycle()方法。如果你调用了recycle()方法之后去尝试绘制这个位图,你将会得到错误:”Canvas: trying to use a recycled bitmap”。

下面的代码是recycle()方法的示例用法。它使用了引用计数的方式来追踪位图当前是处于被展示状态还是被缓存状态。当处于以下状况时,代码会回收位图:

  • 引用数mDisplayRefCount及 mCacheRefCount都是0。
  • 位图不为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中介绍了BitmapFactory.Options.inBitmap属性。如果设置了这个选项,那么解码方法会尝试去重用一个已经存在的位图。这也就是说位图的内存是可重用的,这可以促使改善性能,并且不需要再申请内存及释放内存。然而,inBitmap如何使用还有一些限制,在Android 4.4之前,仅支持相同大小的位图。更多信息,请看文档:inBitmap

保存位图以便稍后使用

下面的代码演示了如何存储一个位图以便稍后使用。当APP运行在Android 3.0以上时,位图会被LruCache移除,接着一个引用位图的软性引用会被放置到一个HashSet中,以便稍后可能被用到:

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()));
            }
        }
    }
....
}

使用已经存在的位图

在运行中的APP内,解码方法会检查是否有已经存在的位图可以被再次使用:

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():

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;
}

最后,这里方法会检查候选位图是否满足大小:

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;
}

下面片段所展示的方法会被上面的片段所调用。它会寻找存在的位图然后将其设置为值。注意这个方法只是对适合的匹配对象设置值。


PS:除了大家对图片的内存关心之外,可能大家还对图片的缓存异步加载加载大图也比较关注。

Note:这篇文章是前面有关位图的所有知识点的综合练习。

上一篇:C语言内存地址基础


下一篇:分享《Linux设备驱动开发详解》第2版高清电子版