在《缓存位图》一课中,我们用缓存的方法提高了位图的重用,除了cache缓存的应用以外,你还可以做其他几个事情来帮助虚拟机进行内存回收和位图重用。根据你的app的目标android版本的不同,我们所推荐的策略也不相同。BitmapFun样例说明了如何在跨越不同版本android的情况下设计app,使app高效的工作。
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 ();
}
在Android2.2(API level 8)以及更低版本,当内存回收发生时,你的app的线程会被停止,导致一定延迟,降低执行效率。android2.3添加了并发的垃圾回收,当位图不在被引用的时候,内存就被回收重用。
在Android2.3.3(API level 10)以及更低版本,位图的pixel data被存储在native memory中,而位图本身是存储在Dalvik heap中。native memory中的pixel data不会以可预知的方式进行释放,因此可能会让一个app超出内存限制而崩溃。Android3.0(API level 11)中pixel data和其关联的位图均存放在Dalvik heap中。
下面章节描述了在不同的Android版本下,如何优化位图内存的管理。
- Android2.3.3及更低版本内存管理
在Android2.3.3(API level 10)及更低版本,推荐用recycle()。如果在你的app中展示大量的位图数据,很可能导致OutOfMemoryError错误。recycle()方法允许app尽可能回收内存。
注:仅当你确定位图数据不会再被使用时,你才可以调用recycle()。如果你先调用了recycle()然后试图重绘这个位图,你会收到错误”Canvas:trying to use a recycled bitmap"。
以下代码片段给出了一个调用recycle()的例子。这个例子用引用计数(mDisplayRefCount和mCacheRefCount)来跟踪一个位图是正在被显示还是在缓存之中。当遇到以下情况这段代码会recycle位图。
1.mDisplayRefCount和mCacheRefCount都是0
2.bitmap不是null,并且它还未被recycle。
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 ();
}
- 在Android3.0及以上版本管理内存
Android3.0(API level 11)引入了BitmapFactory.Options.inBitmap。如果设置了这个选项,解码方法会在加载内容时尝试重用已经存在的bitmap。这意味着bitmap的内存被重用,带来的结果是性能的提升,并且减少了内存分配和内存释放。inBitmap选项有几点注意事项:
1.被重用的bitmap必须和试图加载的源内容的尺寸相同(为了保证内存大小一样),并且必须是JPEG或者PNG格式(无论是以resoucre形式还是stream形式)。
2.被重用的bitmap的configuration会覆盖对inPreferredConfig的设定。
3.你应该坚持使用解码方法返回的bitmap,因为bitmap的重用不保证一定有效(例如,尺寸不匹配)。
为不久将来的使用而保存一个位图
以下的代码片段阐释了如何为不久将来的使用而存储一个bitmap。当一个app是运行在Andorid3.0及更高的版本上,当一个位图从LruCache被剔除出来时,对该位图的一个soft引用会被置于一个HashSet中,以备将来inBitmap的重用。
HashSet<SoftReference<Bitmap>> mReusableBitmaps; private LruCache<String, BitmapDrawable> mMemoryCache; // 如果要运行在Honeycomb或更新的版本上,创建bitmap的引用的HashSet if (Utils.hasHoneycomb()) { mReusableBitmaps = new HashSet<SoftReference<Bitmap>>(); } mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) { // 不再被缓存的条目. @Override protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) { if (RecyclingBitmapDrawable.class.isInstance(oldValue)) { // 被移除的条目是一个正在recycle的图片,所以通知其已经被从缓存中移除 ((RecyclingBitmapDrawable) oldValue).setIsCached(false); } else { // 被移除条目是standard BitmapDrawable. if (Utils.hasHoneycomb()) { // 运行在Honeycomb及更高版本,所以把位图加到HashSet集合 mReusableBitmaps.add (new SoftReference<Bitmap>(oldValue.getBitmap())); } } } .... }
使用已存在的位图
解码器检查是否有能直接用的已存在位图。例如
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()寻找一个已存在的位图为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()) { 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; }
接下来,canUseForInBitmap()决定是否一个候选bitmap能够满足被inBitmap使用的尺寸标准。
private static boolean canUseForInBitmap( Bitmap candidate, BitmapFactory.Options targetOptions) { int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; // Returns true if "candidate" can be used for inBitmap re-use with // "targetOptions". return candidate.getWidth() == width && candidate.getHeight() == height; }