除了缓存bitmap之外,你还能做其他一些事情来优化GC和bitmap的复用。推荐的策略取决于Android的系统版本。附件中的例子会向你展示如何设计app以便在不同的Android版本中提高app的内存性能。
在不同的Android版本中,bitmap的内存管理有所不同。
在Android2.2(api level8)和之前的版本中,当GC触发的时候,App的主线程将会停止。这会导致一个明显的卡顿,并降低用户体验。从Android2.3开始加入了并发GC,这意味着只要bitmap不再被引用,内存将会马上被回收。
在Android2.3.3(api level 10)和以前的版本中,bitmap的像素数据保存在底层内存中,而bitmap本身是保存在虚拟机的heap中。保存在底层内存中的像素数据的回收是不可预测的,这会很容易引起App超过内存限制并且崩溃。在Android3.0以后,bitmap和关联的像素数据都保存在虚拟机的heap中。
下面将介绍在不同的Android版本中如何优化bitmap的内存。
1.在Android2.3.3和之前版本中管理内存。
在Android2.3.3和之前的版本中,推荐使用recycle()方法来管理内存。如果在App中显示很大的图片,将很有可能引起 OutOfMemoryError异常。recycle()方法允许APP尽快回收内存。需要注意的是,只能在你确定bitmap不会再被使用时才能调用recycle()方法。如果后续要显示一个已经调用过recycle()方法的bitmap,你将会得到"Canvas: trying to use a recycled bitmap”异常。
以下的代码片段显示调用recycle()方法的一个例子。这里使用引用计数的方式来跟踪现在正在显示的和正在缓冲中的bitmap(变量mDisplayRefCount和mCacheRefCount)。当满足下面两个条件时将会执行图片回收。
1)mDisplayRefCount和mCacheRefCount同时为0。
2)bitmap不为null,并且没有调用过recycle()方法。
private int mCacheRefCount = 0; private int mDisplayRefCount = 0; ... // drawable的显示状态发生改变. // 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(); }
2.管理Android3.0和更高版本的内存
Android3.0(API level 11)新增了 BitmapFactory.Options.inBitmap字段。如果这个选项被设置,那么使用该Options 的decode方法将会尝试复用一个已经存在的bitmap来加载新的bitmap。这意味着bitmap的内存将被复用,避免分配和释放内存来提升性能。然后,使用inBitmap有一些限制。特别是在Android4.4(API level19)之前,只有尺寸相同的bitmap才能使用该特性。
以下代码段显示如何在APP中保存一个现有的bitmap以便以后的复用。APP运行在Android3.0和更高版本中,当bitmap被LruCache释放,使用一个HashSet来持有该bitmap的一个软引用(soft reference)以便将来通过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())); } } } .... }
在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字段。要注意该方法如果找到合适的bitmap只会设置 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; }
最后,通过下面方法来判断当前的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; }
本文翻译自Android官方文档,原文地址为http://developer.android.com/training/displaying-bitmaps/manage-memory.html