Android中高效的显示图片 - 总结

前面几篇关于高效显示图片的文章已经实现了一个三级缓存、后台加载、裁剪大图的图片加载框架。框架大致如图所示,还有部分知识点图里没有体现(如Activity重建时利用Fragment保存数据),详细情况可以查看之前的文章。完整代码可以点击代码下载。

  1. 计算合适的加载尺寸,避免内存浪费 (加载大图);
  2. 使用后台线程将图片数据加载到内存中 (非UI线程加载);
  3. 通过缓存提高加载后的图片数据的使用率 (图片缓存);
  4. 确认图片不再使用后应尽快释放其所占用的内存空间。



Android AppCompatImageView 获取Bitmap_Android

图片加载流程.png


管理bitmap内存

上面第4条之所以没有链接,是因为它就是本节要讲述的内容。加载图片时所申请的内存位于哪里,当图片不再使用时这部分已经申请的内存能否被其他需要加载的图片直接复用,当内存确实需要释放时又是如何回收的?这些疑问都会在本节内容中找到答案。

随着Android系统版本的不断的更新,Android团队在图片内存管理方面也做了一些优化。

  • 在Android 2.2 (API level 8)及其以下版本上,垃圾回收线程工作时,APP线程就得暂停,这一特性无疑会降低APP的性能。 Android 2.3开始实现了并发垃圾回收,这意味着一个bitmap对象不再任何被引用持有时,它所占有的内存空间会很快的被回收。
  • 在Android 2.3.3 (API level 10)及其以下版本上,bitmap的ARGB数据(backing pixel data)是存在native内存里的,而bitmap对象本身是存在Dalvik的堆里的。当bitmap对象不再被引用时,Dalvik的堆里的内存可以被垃圾回收期回收,但是native部分的内存却不会同步被回收。如果需要频繁的加载很多bitmap到内存中,即使Java层已经及时的释放掉不用bitmap,依旧有可能引起OOM。幸运的是从Android 3.0 (API level 11)开始,bitmap的ARGB数据和bitmap对象一起存在Dalvik的堆里了。这样bitmap对象和它的ARGB数据就可以同步回收了。



Android AppCompatImageView 获取Bitmap_Android_02

Android2.3上bitmap的内存模型




Android AppCompatImageView 获取Bitmap_Android_03

Android3.0上bitmap的内存模型


不同Android版本对bitmap内存管理方式不同,我们应对症下药的来优化不同版本上bitmap的内存使用。

Android 2.3.3 (API level 10)及其以下版本

recycle()方法。recycle()方法可以使APP尽可能快的回收bitmap所使用的native内存。

"Canvas: trying to use a recycled bitmap"的错误。所以调用recycle()方法之前一定要确认bitmap不会再使用了。

下面提供了一个使用recycle()的代码示例。我们使用了引用计数来判断bitmap是否是被显示或者被缓存。当一个bitmap不再被显示也没有被缓存时我们就调用bitmap的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();
}
Android 3.0 (API level 11)及其以上版本

BitmapFactory.Options.inBitmap字段。如果设置了这个字段,bitmap在加载数据时可以复用这个字段所指向的bitmap的内存空间。新增的这种内存复用的特性,可以优化掉因旧bitmap内存释放和新bitmap内存申请所带来的性能损耗。但是,内存能够复用也是有条件的。比如,在Android 4.4(API level 19)之前,只有新旧两个bitmap的尺寸一样才能复用内存空间。Android 4.4开始只要旧bitmap的尺寸大于等于新的bitmap就可以复用了。

下面是bitmap内存复用的代码示例。大致分两步:1、不用的bitmap用软引用保存起来,以备复用;2、使用前面保存的bitmap来创建新的bitmap。

  1. 保存废弃的bitmap
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()));            
             }        
         }    
     }
 ....
 }
  1. 使用现有的废弃bitmap创建新的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中找一个能够被复用的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;
}

canUseForInBitmap()方法用来判断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;
}

完整代码可以点击代码下载。