Android 中解决图像解码导致的 OOM 问题

在上一篇博文 Android Bitmap 内存限制 中我们详细的了解并分析了 Android 为什么会在 Decode Bitmap 的时候出现 OOM 错误,简单的讲就是 Android 在解码图片的时候使用了本地代码来完成解码的操作,但是使用的内存是堆里面的内存,而堆内存的大小是收 VM 实例可用内存大小的限制的,所以当应用程序可用内存已经无法再满足解码的需要时,Android 将抛出 OOM 错误。

这里讲一个题外话,也就是为何 Android 要限制每个应用程序的可用内存大小呢?其实这个问题可能有多方面的解答,目前我自己考虑到的有两点:

  1. 使得内存的使用更为合理,限制每个应用的可用内存上限,可以防止某些应用程序恶意或者无意使用过多的内存,而导致其他应用无法正常运行,我们众所周知的 Android 是有多进程的,如果一个进程 (也就是一个应用) 耗费过多的内存,其他的应用还搞毛呢?当然在这里其实是有一个例外,那就是如果你的应用使用了很多本地代码,在本地代码中创建对象解码图像是不会被计算到的,这是因为你使用本地方法创建的对象或者解码的图像使用的是本地堆的内存,跟系统是平级的,而我们通过 Framework 调用 BitmapFactory.decodeFile() 方法解码时,系统虽然也是调用本地代码来进行解码的,但是 Android Framework 在实现的时候,刻意地将这部分解码使用的内存从堆里面分配了而不是从本地堆里分配的内存,所以才会出现 OOM,当然并不是说从本地堆里分配就不会出现 OOM,本地堆分配内存超过系统可用内存限制的话,通常都是直接崩溃,什么错误可能都看不到,也许会有一些崩溃的错误字节码之类的。
  2. 省电的考虑,呃…,原因我好像也不能很明白地说出来。

回到正题来,我们在应用的设计和开发中可能会经常碰到需要在一个界面上显示数十张图片乃至上百张,当然限于手机屏幕的大小我们通常在设计中会使用类似于列表或者网格的控件来展示,也就是说通常一次需要显示出来图片数还是一个相对确定的数字,通常也不会太大。如果数目比较大的画,通常显示的控件自身尺寸就会比较小,这个时候可以采用缩略图策略。下面我们来看看如果避免出现 OOM 的错误,这个解决方案参考了 Android 示范程序 XML Adapters 中的 ImageDownloader.java 中的实现,主要是使用了一个二级缓存类似的机制, 就是有一个数据结构中直接持有解码成功的 Bitmap 对象引用,同时使用一个二级缓存数据结构持有解码成功的 Bitmap 对象的 SoftReference 对象,由于 SoftReference 对象的特殊性,系统会在需要内存的时候首先将 SoftReference 对象持有的对象释放掉,也就是说当 VM 发现可用内存比较少了需要触发 GC 的时候,就会优先将二级缓存中的 Bitmap 回收,而保有一级缓存中的 Bitmap 对象用于显示。

其实这个解决方案最为关键的一点是使用了一个比较合适的数据结构,那就是 LinkedHashMap 类型来进行一级缓存 Bitmap 的容器,由于 LinkedHashMap 的特殊性,我们可以控制其内部存储对象的个数并且将不再使用的对象从容器中移除,这就给二级缓存提供了可能性,我们可以在一级缓存中一直保存最近被访问到的 Bitmap 对象,而已经被访问过的图片在 LinkedHashMap 的容量超过我们预设值时将会把容器中存在时间最长的对象移除,这个时候我们可以将被移除出 LinkedHashMap 中的对象存放至二级缓存容器中,而二级缓存中对象的管理就交给系统来做了,当系统需要 GC 时就会首先回收二级缓存容器中的 Bitmap 对象了。在获取对象的时候先从一级缓存容器中查找,如果有对应对象并可用直接返回,如果没有的话从二级缓存中查找对应的 SoftReference 对象,判断 SoftReference 对象持有的 Bitmap 是否可用,可用直接返回,否则返回空。

主要的代码段如下:

private static final int HARD_CACHE_CAPACITY = 16;

// Hard cache, with a fixed maximum capacity and a life duration
private static final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
    private static final long serialVersionUID = -57738079457331894L;

    @Override
    protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
        if (size() > HARD_CACHE_CAPACITY) {
            // Entries push-out of hard reference cache are transferred to soft reference cache
            sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
            return true;
        } else
            return false;
    }
};

// Soft cache for bitmap kicked out of hard cache
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);

/**
* @param id
*            The ID of the image that will be retrieved from the cache.
* @return The cached bitmap or null if it was not found.
*/
public Bitmap getBitmap(String id) {
    // First try the hard reference cache
    synchronized (sHardBitmapCache) {
        final Bitmap bitmap = sHardBitmapCache.get(id);
        if (bitmap != null) {
            // Bitmap found in hard cache
            // Move element to first position, so that it is removed last
            sHardBitmapCache.remove(id);
            sHardBitmapCache.put(id, bitmap);
            return bitmap;
        }
    }

    // Then try the soft reference cache
    SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(id);
    if (bitmapReference != null) {
        final Bitmap bitmap = bitmapReference.get();
        if (bitmap != null) {
            // Bitmap found in soft cache
            return bitmap;
        } else {
            // Soft reference has been Garbage Collected
            sSoftBitmapCache.remove(id);
        }
    }

    return null;
}

public void putBitmap(String id, Bitmap bitmap) {
    synchronized (sHardBitmapCache) {
        if (sHardBitmapCache != null) {
            sHardBitmapCache.put(id, bitmap);
        }
    }
}

上面这段代码中使用了 id 来标识一个 Bitmap 对象,这个可能大家在实际的应用中可以选择不同的方式来索引 Bitmap 对象,图像的解码在这里就不做赘述了。这里主要讨论的就是如何管理 Bitmap 对象,使得在实际应用中不要轻易出现 OOM 错误,其实在这个解决方案中,HARD_CACHE_CAPACITY 的值就是一个经验值,而且这个跟每个应用中需要解码的图片的实际大小直接相关,如果图片偏大的话可能这个值还得调小,如果图片本身比较小的话可以适当的调大一些。本解决方案主要讨论的是一种双缓存结合使用 SoftReference 的机制,通过使用二级缓存和系统对 SoftReference 对象的回收特性,让系统自动回收不再敏感的图片 Bitmap 对象,而保有一级缓存也就是敏感的图片 Bitmap 对象。

  • 桀骜

    能留言吗?

  • 桀骜

    楼主 你的这篇文章讲解的很好,学习了很多,对你后面提出的解决方案也很有兴趣,研究了你给出的方案代码想试试能不能真正解决问题,请问方便能给个 demo 吗?关于这个的,也就是完整的代码使用例子,邮箱 orosem625@163.com 盼复。

    • 这个思路借鉴的是 http://developer.android.com/resources/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.html
      这个地方的一段代码,读懂了那段代码的思路,也就可以尝试我在文章中描述的方法了,至于具体的代码,我这边还真没有,因为代码都是公司实际项目中的,不太方便拿出来。

  • 桀骜

    恩 理解,谢谢你的分享,你这个方法我也试着使用了,效果不错哦~

  • 桀骜

    你好,lz 我突然想到一个问题想请教下你,在你的这实现思路里能不能实现我下面所说的我想要的功能。在第一级的 LinkedHashMap 类中能不能我固定让一个 bitmap 对象不被淘汰,因为我现在会有一个图片会被反复的从 sdcard 中读取,这样对内存消耗太大,我想让他一直放在 LinkedHashMap 中不被淘汰掉,有没有什么可以让他固定在里面呢?辛苦了。

    • 如果你真的很需要固定某个 Bitmap 对象不被淘汰,例如你在游戏中需要使用到的背景图之类的,那么你就不应该采用这种结构来存储你的这个 Bitmap 对象,应该由其他的数据结构来控制,这个结构目前适用的场景就是流式阅读照片的

  • zddd

    博主,请问一下,我用 SoftReference 时,即使不多图片(远小于 16M),也有很多会被 GC 掉,是什么原因呢?这样就导致我的二级缓存里面就只有那么几个 bitmap,基本上没啥作用… list item 一多,就有很多 item 上的图片是空白了…
    谢谢

    • 因为程序中使用 SoftReference 就意味着你对这些东西已经不敏感了,你不是很需要这些东西,告知系统随时可以回收。所以这个问题还是在业务逻辑层上,我想你的这个问题应该还是通过上层控制那些 Bitmap 需要放到一级缓存那些需要放到二级缓存中,这样会更好一些,而且每应用需求不太一致,所以我的建议是你把一级缓存的大小调大一些,这样就不会频繁的在二级缓存中出现抖动的情况了

  • 古月

    楼主你分析的很详细,这个方法也比许多网上搜到的教程要实际有效很多。可是很奇怪的是,我应用中(类似于相册)已经使用了此方法,但随着浏览的图片越来越多,内存还是不断的增加(比没用此方法的时候情况好很多),最终还是会 OOM~

    • 每个人的应用场景中对照片大小的需求都不是一样的,所以每个人碰到的问题可能也会不一样,具体的两个缓存容器中最多能放多少的 Bitmap 对象还是取决于实际情况的,需要根据应用场景进行调整的,而且在使用一级缓存的时候可以增加内存超过多少也将一级缓存中的 Bitmap 对象移除,放到二级缓存中。另外在进行解码的时候很有必要针对实际需要大小进行解码,尽可能在源头处控制 Bitmap 的大小而不只是依赖于一个内存管理的机制

  • shelley001

    软引用已不被官方推荐使用

    • 该好好看看官方文档了,:-)

  • yueang

    缓存的作用是为了让加载更快吧,能够节省内存吗?如果我在 Adapter 里对当前显示的 view 需要的图片每次都重新 decode,如果不再显示的话就立即 recyle,也不会引起内存问题吧,感觉这个方法在解决 OOM 方面也就是 SoftReference 有些用吧?

  • dfqin

    google 文档中有四篇专门讲图片加载的,其中有一往篇提到” 注意:这前,很流行的图片缓存的方法是使用 SoftReference 和 WeakReference,但是这种方法提倡。因为从 Android2.3(Level 9) 开始,内存回收器会对软引用和弱引用进行回收。另外,在 Android3.0(Levle 11) 之前,图片的数据是存储在系统 native 内存中的,它的内存释放不可预料,这也是造成程序内存溢出的一个潜在原因。” 如果用缓存的话,官方推荐使用 LruCache。不过 OOM 的问题始终无解,关键设备太杂了,很郁闷。

  • dfqin

    应该是“不值得提倡”上面打错了。。