首页 > 代码库 > 高效地加载图片(四) 管理缓存

高效地加载图片(四) 管理缓存

 

除了缓存图片意外,还有一些其他的方式来促进GC的效率和图片的复用.不同的Android系统版本有不同的处理策略.BitmapFun中就包含了这个类,能够使我们高效地构建我们的项目.

为了开始以下教程,我们需要先介绍一下Android系统对Bitmap管理的进化史.

  • 在Android2.2(API level 8)以及更低的版本中,当垃圾被回收时,应用的线程会被停止,这会造成一定程度的延时.在Android 2.3中,加入了并发回收机制,这意味着当Bitmap不再被使用的时候,内存会被很快地回收.
  • 在Android 2.3.3(API level 10)以及更低版本中,像素数据是被存储在本地内存中的,并且和Bitmap本身是分离的,Bitmap存储在Dalvik堆中.本地内存中的像素数据并不以一种可预知的方式释放,可能会造成应用内存溢出和崩溃.而在Android 3.0(API level 11)中,像素数据和Bitmap一起被保存在Dalvik堆中.

以下内容描述了如何为不同的Android版本选择最佳的图片缓存管理方式.

在Android 2.3.3以及更低版本中管理缓存

在Android 2.3.3(API level 10)以及更低版本中,建议使用Bitmap.recycle()方法.如果你正在展示大量的图片,应用可能会内存溢出.这时候recycle()方法可以使内存被尽可能快地回收.

注意:你只能在确定了Bitmap确实不再被使用了之后才能调用recycle()方法.如果你调用recycle()方法释放了Bitmap,而稍后却又使用这个Bitmap,这时就会出现"Canvas: trying to use a recycled bitmap"错误.

以下代码片段是recycle()方法调用的示例.这里使用了引用计数器的方式(通过变量mDisplayRefCount和mCacheRefCount)来追踪一个Bitmap是否还在被使用或者存在于缓存中.当满足如下条件时,此处就会回收Bitmap:

  • 引用计数器变量mDisplayRefCount和mCacheRefCount的值都为0时.
  • Bitmap不为null,而且没有被回收.
view plaincopy to clipboardprint?
  1. private int mCacheRefCount = 0;  
  2. private int mDisplayRefCount = 0;  
  3. ...  
  4. // 通知当前Drawable,当前被使用的状态改变了  
  5. // 保持一个计数器,用来决定当前Drawable何时被回收  
  6. public void setIsDisplayed(boolean isDisplayed) {  
  7.     synchronized (this) {  
  8.         if (isDisplayed) {  
  9.             // 当被显示时,则计数器mDisplayRefCount的值+1  
  10.             mDisplayRefCount++;  
  11.             // 标志当前Drawable被显示了  
  12.             mHasBeenDisplayed = true;  
  13.         } else {  
  14.             // 当一个引用被释放时,计数器mDisplayRefCount的值-1  
  15.             mDisplayRefCount--;  
  16.         }  
  17.     }  
  18.     // 确认recycle()方法是否已经被调用了  
  19.     checkState();  
  20. }  
  21.   
  22. // 通知当前Drawable缓存状态改变了  
  23. // 保持一个计数器用于决定何时这个Drawable不再被缓存  
  24. public void setIsCached(boolean isCached) {  
  25.     synchronized (this) {  
  26.         if (isCached) {  
  27.             mCacheRefCount++;  
  28.         } else {  
  29.             mCacheRefCount--;  
  30.         }  
  31.     }  
  32.     // 确认recycle()方法是否已经被调用了  
  33.     checkState();  
  34. }  
  35.   
  36. private synchronized void checkState() {  
  37.     // 如果当前Drawable的内存和展示计数器都为0,而且当前Drawable还可用  
  38.     // 则释放掉它  
  39.     if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed  
  40.             && hasValidBitmap()) {  
  41.         getBitmap().recycle();  
  42.     }  
  43. }  
  44.   
  45. // 判断Drawable对应的Bitmap是否可用  
  46. private synchronized boolean hasValidBitmap() {  
  47.     Bitmap bitmap = getBitmap();  
  48.     return bitmap != null && !bitmap.isRecycled();  
  49. }  

在Android 3.0以及更高版本中管理缓存

 

从Android 3.0(API level 11)开始,引入了BitmapFactory.Options.inBitmap字段,如果这个属性被设置了,拥有这个Options对象的方法在解析图片的时候会尝试复用一张已存在的图片.这意味着图片缓存被服用了,这意味着更流畅的用户体验以及更好的内存分配和回收.然而,要使用inBitmap有这一定的限制.尤其需要注意的是,在Android 4.4(API level 19)之前,只有相同大小的Btiamp才会被复用.更详细的用法,请参见inBitmap的文档.

保存一个Bitmap以备后来使用

下面的代码片段示范了如何保存一个Bitmap以备后来使用.当在Android 3.0以及更高版本平台中时,一个Bitmap被从LrcCache中移除后,它的软引用会被保存到一个HashSet中,以备inBitmap后来使用:

view plaincopy to clipboardprint?
  1. Set<SoftReference<Bitmap>> mReusableBitmaps;  
  2. private LruCache<String, BitmapDrawable> mMemoryCache;  
  3.   
  4. // 当程序运行在Honeycomb(Android 3.1)及以上版本的系统中时,创建一个  
  5. // 同步的HashSet用于存放可复用的Bitmap的引用  
  6. if (Utils.hasHoneycomb()) {  
  7.     mReusableBitmaps =  
  8.             Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());  
  9. }  
  10.   
  11. mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {  
  12.   
  13.     // Notify the removed entry that is no longer being cached.  
  14.     @Override  
  15.     protected void entryRemoved(boolean evicted, String key,  
  16.             BitmapDrawable oldValue, BitmapDrawable newValue) {  
  17.         if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {  
  18.             // 如果被移除的Drawable是RecyclingBitmapDrawable,则通知它  
  19.             // 已经被从内存缓存中移除  
  20.             ((RecyclingBitmapDrawable) oldValue).setIsCached(false);  
  21.         } else {  
  22.             // 如果被移除的Drawable是一个普通的BitmapDrawable  
  23.             if (Utils.hasHoneycomb()) {  
  24.                 // 如果运行在Honeycomb(Android 3.1)及以上系统中,则为Bitmap添加  
  25.                 // 到一个保存软引用的集合中,以备后来被inBitmap使用  
  26.                 mReusableBitmaps.add  
  27.                         (new SoftReference<Bitmap>(oldValue.getBitmap()));  
  28.             }  
  29.         }  
  30.     }  
  31. ....  
  32. }  

复用一个已经存在的Bitmap

在解析图片时,会检查是否已经存在一个可用的Bitmap,如下:

view plaincopy to clipboardprint?
  1. public static Bitmap decodeSampledBitmapFromFile(String filename,  
  2.         int reqWidth, int reqHeight, ImageCache cache) {  
  3.   
  4.     final BitmapFactory.Options options = new BitmapFactory.Options();  
  5.     ...  
  6.     BitmapFactory.decodeFile(filename, options);  
  7.     ...  
  8.   
  9.     // 如果应用运行在Honeycomb及以上系统中,则尝试使用inBitmap复用图片  
  10.     if (Utils.hasHoneycomb()) {  
  11.         addInBitmapOptions(options, cache);  
  12.     }  
  13.     ...  
  14.     return BitmapFactory.decodeFile(filename, options);  
  15. }  

以下代码片段描述了如何查找一个已经存在的Bitmap并将它设置为inBitmap的值.注意,这个方法只有在找到了符合条件的Bitmap才会为inBitmap赋值(我们不能乐观地假定图片会被找到):

view plaincopy to clipboardprint?
  1. private static void addInBitmapOptions(BitmapFactory.Options options,  
  2.         ImageCache cache) {  
  3.     // inBitmap仅能接受可编辑的Bitmap,所以此处需要强制解码器  
  4.     // 返回一个可编辑的Bitmap  
  5.     options.inMutable = true;  
  6.   
  7.     if (cache != null) {  
  8.         // 尝试查找满足条件的Bitmap给inBitmap赋值  
  9.         Bitmap inBitmap = cache.getBitmapFromReusableSet(options);  
  10.   
  11.         if (inBitmap != null) {  
  12.             // 如果查找到了符合条件的Bitmap,则赋值给inBitmap  
  13.             options.inBitmap = inBitmap;  
  14.         }  
  15.     }  
  16. }  
  17.   
  18. // 这个方法遍历了保存弱引用的集合,用于查找一个合适的Bitmap  
  19. protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {  
  20.         Bitmap bitmap = null;  
  21.   
  22.     if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {  
  23.         synchronized (mReusableBitmaps) {  
  24.             final Iterator<SoftReference<Bitmap>> iterator  
  25.                     = mReusableBitmaps.iterator();  
  26.             Bitmap item;  
  27.   
  28.             while (iterator.hasNext()) {  
  29.                 item = iterator.next().get();  
  30.   
  31.                 if (null != item && item.isMutable()) {  
  32.                     // 检查当前的Bitmap是否满足inBitmap的复用条件  
  33.                     // Check to see it the item can be used for inBitmap.  
  34.                     if (canUseForInBitmap(item, options)) {  
  35.                         // 如果满足复用条件,则给Bitmap赋值,并在方法结束时返回  
  36.                         bitmap = item;  
  37.                         // 在给返回值赋值后,将当前Bitmap的引用从集合中移除  
  38.                         iterator.remove();  
  39.                         break;  
  40.                     }  
  41.                 } else {  
  42.                     // 如果读取到的是空引用,则将该引用移除  
  43.                     iterator.remove();  
  44.                 }  
  45.             }  
  46.         }  
  47.     }  
  48.     return bitmap;  
  49. }  

最后,下面的这个方法能够检查哪个Bitmap满足inBitmap的复用条件:

view plaincopy to clipboardprint?
  1. static boolean canUseForInBitmap(  
  2.         Bitmap candidate, BitmapFactory.Options targetOptions) {  
  3.   
  4.     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {  
  5.         // 从Android 4.4(KitKat)开始,如果当前需要的Bitmap尺寸(内存中占用的字节数)比缓存中的Bitmap尺寸小  
  6.         // 并且同一张图片的Bitmap存在,我们就可以复用它  
  7.         int width = targetOptions.outWidth / targetOptions.inSampleSize;  
  8.         int height = targetOptions.outHeight / targetOptions.inSampleSize;  
  9.         // 当前需要的Bitmap占用字节数  
  10.         int byteCount = width * height * getBytesPerPixel(candidate.getConfig());  
  11.         // 如果需要的Bitmap尺寸小于原Bitmap,则返回true  
  12.         return byteCount <= candidate.getAllocationByteCount();  
  13.     }  
  14.   
  15.     // 在Android 4.4以前,尺寸必须完全一致,并且inSampleSize为1时  
  16.     // Bitmap才能被复用  
  17.     return candidate.getWidth() == targetOptions.outWidth  
  18.             && candidate.getHeight() == targetOptions.outHeight  
  19.             && targetOptions.inSampleSize == 1;  
  20. }  
  21.   
  22. /** 
  23.  * 一个用于判断在不同的参数下,每个像素占用字节数的方法 
  24.  */  
  25. static int getBytesPerPixel(Config config) {  
  26.     if (config == Config.ARGB_8888) {  
  27.         return 4;  
  28.     } else if (config == Config.RGB_565) {  
  29.         return 2;  
  30.     } else if (config == Config.ARGB_4444) {  
  31.         return 2;  
  32.     } else if (config == Config.ALPHA_8) {  
  33.         return 1;  
  34.     }  
  35.     return 1;  
  36. }