最近分析线上日志,发现存在一定量的OutOfMemoryError。由于Android系统对堆内存大小作了限制,不同的设备上这个阈值也会不同,当已分配内存加新分配内存大于堆内存就会导致OOM。虽然Android机型的配置在不断升级,但还是存在着几年前的旧机型,它们的特点是内存小,尤其在涉及大图片加载时很容易出现OOM。
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;
}
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;
}
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;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
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;
}
}
}
}
改进前内存保持在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
本文来自网易实践者社区,经作者潘威授权发布