Android中高效的显示图片 - Bitmap的内存模型

相对于文字来说,图片的表达更直接、更有冲击力、更容易吸引用户的眼球。设计师们也理所当然的喜欢用图片来传达信息。但是对于开发者来说,图片就意味着大量的内存开销。要想APP在性能上有更好的表现,我们必须处理好显示图片所需要的每个环节。

(本文出处:http://www.jianshu.com/p/eadb0ef271b0)

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

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

  1. 计算合适的加载尺寸,避免内存浪费 (加载大图)
  2. 使用后台线程将图片数据加载到内存中 (非UI线程加载)
  3. 通过缓存提高加载后的图片数据的使用率 (图片缓存)
  4. 确认图片不再使用后应尽快释放其所占用的内存空间。
图片加载流程.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数据就可以同步回收了。
Android2.3上bitmap的内存模型
Android3.0上bitmap的内存模型

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

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

在Android 2.3.3 (API level 10)及其以下版本上,Android开发文档推荐我们使用 recycle()方法。recycle()方法可以使APP尽可能快的回收bitmap所使用的native内存。

注意:recycle()方法是不可逆的,bitmap调用了recycle()之后就不能再使用了。使用recycle()之后的bitmap系统会抛出"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)及其以上版本

Android 3.0 开始引入了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;
    }

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容