本文来自网易云社区。

最近分析线上日志,发现存在一定量的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操作,可能会造成程序性能的整体下降。因此,需要在实践中进行综合考虑做一定的权衡。 参考资料

jianshu.com/p/f5d8d3066

my.oschina.net/u/586684bozhiyue.com/anroid/bokblog.chinaunix.net/uid-developer.android.com/i

原文:Android图片缓存分析与优化,经作者潘威授权发布

了解网易云 :

网易云官网:https://www.163yun.com

网易云社区:sq.163yun.com/blog

更多网易研发、产品、运营经验分享请访问网易云社区。

推荐阅读:

相关文章