Android图片缓存分析与优化

达芬奇密码2018-06-21 17:46

        最近分析线上日志,发现存在一定量的OutOfMemoryError。由于Android系统对堆内存大小作了限制,不同的设备上这个阈值也会不同,当已分配内存加新分配内存大于堆内存就会导致OOM。虽然Android机型的配置在不断升级,但还是存在着几年前的旧机型,它们的特点是内存小,尤其在涉及大图片加载时很容易出现OOM。

概述
        为了避免OOM,程序应该增加可用内存,并及时回收不再使用的对象,降低内存占用。可以从以下几个方面去考虑:
        1、对图片进行处理,如图片裁剪和压缩。
        使用缩略图来提高加载速度和降低内存占用。根据控件大小对图片进行裁剪,减少不必要内存浪费。我们的项目中使用了NOS提供的图片处理服务,它提供了非常强大的云处理功能,在开发过程中根据实际需要生成请求链接,获取不同尺寸的图片,实现图片裁剪。同时使用BitmapFactory.Options属性,通过设置采样率, 减少Bitmap的像素。
        2、内存引用上做一些处理,常用的有软引用。
        使用软引用的对象在内存足够时,垃圾回收器不会回收它;当内存空间不足时,为满足程序运行的需求,会回收这些对象,避免出现OOM导致的程序崩溃。因此只要对象没有被回收都能被程序使用。
        过去很多应用都大量使用软引用进行图片缓存,通过GC自动回收图片所占内存。从Android 2. 3开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,在Android 3.0中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃,所以在开发过程中要谨慎使用。
        3、缓存机制。
        缓存不仅可以减少流量的浪费还能防止加载过多的图片,项目中使用了比较主流的内存、文件和网络三级缓存。
        通过URL向网络请求图片时,先从内存中查找,如果内存中没有,再从缓存文件中查找,如果缓存文件中也没有,再向网络发Http请求下载图片,然后再依次缓存在内存和文件中。
        在项目中使用了强引用(LRUCache)与软引用相结合的方式进行内存缓存。系统不会回收强引用的对象,为了防止OOM,需要为LRUCache设置适当的大小,并及时回收内存。因为堆空间又被分为年轻代、老年代和永久代,新分配的对象会先放在年轻代中,当停留一段时间后,这个对象会被移动到老年代,最后再移动到永久代中。系统的每一个内存区域都采用不同的策略进行GC操作,年轻代的对象更容易被销毁,而且GC操作的速度比老年代的速度要快,时间更短。
        同时Android系统并不会对空闲内存区域做碎片整理,只有在内存不足时触发GC进行回收,从而造成空间上的浪费。因此,程序应该在适当的时候主动回收不再使用的图片,减少被动回收导致的内存溢出风险。
        4、自定义堆内存大小,如使用largeHeap。
        在Manifest.xml中的Application节点下加入android:largeHeap="true",系统便能为应用程序分配更多的内存空间,但是这种方式不能根本解决问题,不合理的使用内存同样会造成OOM,只是延缓其发生。对于一些内存占用比较大的图片、视频类应用,最好在开发测试过后再加上该属性。
        5、使用第三方开源图片框架,比如Picasso、Glide、Fresco等,它们在图片异步加载、缓存、内存管理和优化等方面已经做了很好的处理。

基于LRUCache的缓存
        前面介绍了几种避免OOM的方式,在实际项目中需要结合使用。本文主要介绍内存缓存的实现,包括强引用缓存和软引用缓存两个部分。强引用缓存采用LRUCache实现,它是Android系统为开发人员提供的缓存工具类,实际上是将强引用的对象存储在LinkedHashMap中,初始化时会设置缓存空间大小,当缓存数据达到预设值时会采用最近最少使用算法进行淘汰。另外,软引用缓存同样使用LinkedHashMap作为存储结构,将从LRUCache淘汰的数据扔到软引用缓存中,之前的做法是对软引用对象不做任何处理,等待垃圾回收器自动回收。大量使用软引用的弊端前面也有介绍,本文对此做了部分改进,有限的使用软引用对象,当软引用缓存空间不足时,同样按照LRU规则淘汰并主动回收内存空间。
        
        首先,通过图片的URL从网络下载图片,将图片先缓存到内存缓存中,缓存到强引用也就是LruCache中。如果LruCache空间不足,就会将较早存储的图片对象淘汰到软引用缓存中,然后将图片缓存到文件中。在读取图片时,先读取内存缓存,判断LruCache是否存在图片,如果存在,则直接读取,如果LruCache中不存在,则判断软引用中是否存在,如果软引用中存在,则将软引用中的图片添加到LruCache并且删除软引用中的数据,如果软引用中不存在,则从文件或网络读取。

代码分析与实现
        首先,通过继承LRUCache类实现BitmapLRUCache,里面的键值对分别是URL和对应图片的Drawable对象。
class BitmapLRUCache extends LruCache<String, Drawable>
        然后在构造方法中初始化软引用缓存mSoftBitmapCache,并设置LRUCache的大小,这里设置为手机可用内存的1/4。
int maxMemory=(int)Runtime.getRuntime().maxMemory()/4;

        通过LRUCache构造方法的源码可以看出,实际上是初始化一个LinkedHashMap,并且LinkedHashMap中的对象采用LRU规则自动排序。

public LruCache(int maxSize) {
    ... ...
    this.maxSize = maxSize;
    this.map = new LinkedHashMap(0, 0.75f, true);
}
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    init();
    this.accessOrder = accessOrder;
}
        在LruCache中初始化LinkedHashMap构造方法的accessOrder参数值为true,这个参数默认为false,表示对象按照插入顺序排序。下面是LruCache类的get方法:
 public V get(Object key) {
    ... ...
    for (HashMapEntry e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            if (accessOrder)
                makeTail((LinkedEntry) e);
            return e.value;
        }
    }
    return null;
}
        当向缓存get数据时,如果accessOrder为true,则通过makeTail((LinkedEntry) e)方法将对象移到了末尾, 这样就能够保证每次从头部移除最近最少使用的对象。
        如果向LRUCache中插入图片对象,当缓存空间不足时,需要移除最近最少使用对象,由于LinkedHashMap已经做好了排序, 所以直接移除头部对象即可。
       下面是LRUCache的put方法:
public final V put(K key, V value) {

... ... V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; }

        其中safeSizeOf(key, value)用来获取待插入对象的大小,并对已占用内存进行累加。再看看这个方法: 
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}


        返回的result通过sizeOf(key, value)这个方法获取,到这里我们明白了需要重写sizeOf方法, 这里用每行像素点所占用的字节数乘高度计算出图片大小。 
protected int sizeOf(String key, Drawable value) {
if(value!=null) {
if (value instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
return bitmap.getRowBytes() * bitmap.getHeight();
}
return 1;
}else{
return 0;
}
}


        然而真正移除对象是在trimToSize(maxSize)这个方法中:

public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
... ...
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}


        这里会检查当前缓存容量,size <= maxSize便移除头部对象。最后调用了entryRemoved(true, key, value, null)方法。

        因此,我们可以重写该方法来处理淘汰对象:

protected void entryRemoved(boolean evicted, String key, Drawable oldValue, Drawable newValue) {
   if (evicted) {
      if (oldValue != null) {
         //当硬缓存满了,根据LRU规则移入软缓存
         synchronized(mSoftBitmapCache) {
            mSoftBitmapCache.put(key, new SoftReference(oldValue));
         }
      }
   }else{//主动移除,回收无效空间
      recycleDrawable(oldValue);
   }
}

        当evicted变量为true时,属于为腾出缓存空间被调用,将被淘汰的对象插入软引用缓存mSoftBitmapCache中。

        当evicted变量为false时,属于主动淘汰对象,看下面代码:

public final V remove(K key) {
    ... ...
    V previous;
    synchronized (this) {
        previous = map.remove(key);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    if (previous != null) {
        entryRemoved(false, key, previous, null);
    }
    return previous;
}

        entryRemoved方法在LRUCache的remove方法中调用时,evicted参数的值为false,因此这里直接回收图片对象。 

        如果软引用缓存mSoftBitmapCache超出上限,也根据LRU规则进行淘汰,直接回收对象的内存空间。这里参考LRUCache的实现方式进行初始化:

this.mSoftBitmapCache= new LinkedHashMap>(SOFT_CACHE_SIZE, 0.75f, true){ 
   @Override
   protected boolean removeEldestEntry(Entry> eldest) {
      if (size() > SOFT_CACHE_SIZE) {//缓存数量不超过10
         if(eldest!=null){
            SoftReference bitmapReference=eldest.getValue();
            if(bitmapReference!=null){
               Drawable oldValue=bitmapReference.get();
               recycleDrawable(oldValue);
            }
         }
         return true;
      }
      return false;
   }
};

        不同的是重写了removeEldestEntry方法,这个方法主要用于判断缓存容量是否超过上限,如果超出则回收被淘汰的对象。

        再看看LinkedHashMap类的put方法调用了addNewEntry方法,在该方法中会根据removeEldestEntry方法的返回来决定是否移除对象:

public V put(K key, V value) {
        ... ...
    addNewEntry(key, value, hash, index);
    return null;
}
void addNewEntry(K key, V value, int hash, int index) {
    LinkedEntry header = this.header;
    // Remove eldest entry if instructed to do so.
    LinkedEntry eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) {
       remove(eldest.key);
    }
        ......
}

        因此,当size() > SOFT_CACHE_SIZE时,便对老对象进行移除操作。 从缓存中获取对象的方法:

   public Drawable getBitmap(String url){
      // 先从硬缓存中获取
      Drawable bitmap = get(url);
      if (bitmap != null) {
         return bitmap;
      }
      synchronized (mSoftBitmapCache) {
         SoftReference bitmapReference = mSoftBitmapCache.get(url);
         if (bitmapReference != null) {
            bitmap = bitmapReference.get();
            if (bitmap != null) {
               //移入硬缓存
               put(url, bitmap);
               mSoftBitmapCache.remove(url);
               return bitmap;
            } else {
               mSoftBitmapCache.remove(url);
            }
         }
      }
      return null;
   }

        优先从硬缓存中拿,如果存在则返回。否则查询软引用缓存,存在则返回对象并移入硬缓存中。

        最后上完整的代码:

public class BitmapLRUCache extends LruCache {
   private final int SOFT_CACHE_SIZE = 10; // 软引用缓存容量
   private LinkedHashMap> mSoftBitmapCache;//软引用缓存,已清理的数据可能会再次使用
   public BitmapLRUCache(int maxSize) {
      super(maxSize);
      this.mSoftBitmapCache= new LinkedHashMap>(SOFT_CACHE_SIZE, 0.75f, true){// true 采用LRU排序,移除队首
         @Override
         protected boolean removeEldestEntry(Entry> eldest) {
            if (size() > SOFT_CACHE_SIZE) {//缓存数量不超过10
               if(eldest!=null){
                  SoftReference bitmapReference=eldest.getValue();
                  if(bitmapReference!=null){
                     Drawable oldValue=bitmapReference.get();
                     recycleDrawable(oldValue);
                  }
               }
               return true;
            }
            return false;
         }
      };
   }
   public Drawable getBitmap(String url){
      // 先从硬缓存中获取
      Drawable bitmap = get(url);
      if (bitmap != null) {
         return bitmap;
      }
      synchronized (mSoftBitmapCache) {
         SoftReference bitmapReference = mSoftBitmapCache.get(url);
         if (bitmapReference != null) {
            bitmap = bitmapReference.get();
            if (bitmap != null) {
               //移入硬缓存
               put(url, bitmap);
               mSoftBitmapCache.remove(url);
               return bitmap;
            } else {
               mSoftBitmapCache.remove(url);
            }
         }
      }
      return null;
   }
   private  int getSizeInBytes(Bitmap bitmap) {
      int size = bitmap.getRowBytes() * bitmap.getHeight();//每一行像素点所占用的字节数 *  高度
      return size;
   }
   protected int sizeOf(String key, Drawable value) {
      if(value!=null) {
         if (value instanceof BitmapDrawable) {
            Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
            return getSizeInBytes(bitmap);
         }
         return 1;
      }else{
         return  0;
      }
   }
   protected void entryRemoved(boolean evicted, String key, Drawable oldValue, Drawable newValue) {
      super.entryRemoved(evicted, key, oldValue, newValue);
      if (evicted) {
         if (oldValue != null) {
            //当硬缓存满了,根据LRU规则移入软缓存
            synchronized(mSoftBitmapCache) {
               mSoftBitmapCache.put(key, new SoftReference(oldValue));
            }
         }
      }else{//主动移除,回收无效空间
         recycleDrawable(oldValue);
      }
   }
   private void recycleDrawable(Drawable oldValue) {
      if (oldValue != null) {
         try {
            if (oldValue instanceof BitmapDrawable) {
               Bitmap bitmap = ((BitmapDrawable) oldValue).getBitmap();
               bitmap.recycle();
            }
            Log.i("BitmapLRUCache", "oldValue:" + oldValue);
         } catch (Exception exception) {
            Log.i("BitmapLRUCache", "Failed to clear Bitmap images on close", exception);
         } finally {
            oldValue = null;
         }
      }
   }
}

测试
        测试机器为华为G525,系统版本为4.1。运行改进后的代码,在AndroidStudio中查看Monitors栏,启动程序并进行简单操作,很清楚的看到内存占用的实时变化以及释放的过程。

        改进前内存保持在30M到40M之间,并且通过log日志观察GC暂停时间相对较长。改进后内存保持在20M以下。测试结果,有效的降低了内存占用。


总结

        应用程序过高的内存占用,资源不能及时释放,容易导致OOM。但内存占用也不是越少就越好,如果为了保持较低的内存占用而频繁触发GC操作,可能会造成程序性能的整体下降。因此,需要在实践中进行综合考虑做一定的权衡。


参考资料

http://www.jianshu.com/p/f5d8d3066b36

https://my.oschina.net/u/586684/blog/226056

http://www.bozhiyue.com/anroid/boke/2016/0521/132735.html

http://blog.chinaunix.net/uid-26930580-id-4138306.html

https://developer.android.com/index.html

本文来自网易实践者社区,经作者潘威授权发布