看看開源庫Universal Image Loader中存在的Bug
話說幾年前,Universal Image Loader是安卓應用中圖片加載的主流開源庫,很多千萬級的應用中都可以看到它的身影。前段時間抱著學習的態度,把Universal Image Loader的源碼下載下來,準備領略一下國際大牛的風采,結果意外的發現了一個Bug,由于項目已經停止維護了,也就沒有pull request。最近突然喜歡上了寫博客,所以今天就把它的代碼貼上了,和大家一起學習一下。(幾年前應用搞不好就會OOM,會不會就是因為這個bug啊。不管是不是,大家先來看代碼分析吧)
這個bug是內存策略中存在的bug,會引起OOM。我們的分析會涉及以下四個類:
四個類的繼承關系,請原諒我畫的這么丑陋
其中MemoryCache中,就五個方法,很簡單:
MemoryCache.png
在BaseMemoryCache中有成員變量:HashMap,它存儲的值是Reference的Bitmap。其實問題就出在這里了,我們繼續往下看。大家別著急,我文章最后會把四個類的全部代碼貼上來,供大家仔細看。根據他的注釋可以看出,要存儲一個非強引用的對象,也就是弱引用或軟引用。
HashMap
我們再來看看他的put方法,到了往HashMap里放了什么。根據下面代碼可以看出是調用了createReference()這個抽象方法,交給子類去實現了。
BaceMemoryCache的put方法
BaseMemoryCache的直接子類LimitedMemoryCache也是一個抽象類,沒有實現createReference()方法,在他的間接子類UsingFreqLimiteMemory中實現了.可以看出方法中創建了一個Bitmap的弱引用。弱引用就是只要發生GC,對象都有可能被回收。
UsingFreqLimiteMemory的createReference()
重點來啦,我們看UsingFreqLimitedMemory中的remove方法。它掉用的是父類中的get方法,由于直接父類LimitedMemoryCache中沒有實現get方法,所以它調用的是BaseMemoryCache中的get方法。
UsingFreqLimitedMemory
我們來看看BaseMemoryCache中的get方法:
BaseMemoryCache
get方法就是通過成員變量HashMap獲得BitMap。由于存儲的值是一個弱引用,所以可能隨時被GC。那么它子類就可能拿不到這個Bitmap的引用了,并且兩個子類中都有HashMap保存著對應的Bitmap,所以子類中持有的Bitmap就永遠無法移除了,直到程序發生OOM。
下面就把全部代碼貼上來,大家可以仔細研究一下,共四個類由父類到子類。歡迎大家在下面討論交流。
/**
* Interface for memory cache
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.9.2
*/
public interface MemoryCache {
/**
* Puts value into cache by key
*
* @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> put into
* cache
*/
boolean put(String key, Bitmap value);
/** Returns value by key. If there is no value for key then null will be returned. */
Bitmap get(String key);
/** Removes item by key */
Bitmap remove(String key);
/** Returns all keys of cache */
Collection<String> keys();
/** Remove all items from cache */
void clear();
}
/**
* Base memory cache. Implements common functionality for memory cache. Provides object references (
* {@linkplain Reference not strong}) storing.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.0.0
*/
public abstract class BaseMemoryCache implements MemoryCache {
/** Stores not strong references to objects */
private final Map<String, Reference<Bitmap>> softMap
= Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());
@Override
public Bitmap get(String key) {
Bitmap result = null;
Reference<Bitmap> reference = softMap.get(key);
if (reference != null) {
result = reference.get();
}
return result;
}
@Override
public boolean put(String key, Bitmap value) {
softMap.put(key, createReference(value));
return true;
}
@Override
public Bitmap remove(String key) {
Reference<Bitmap> bmpRef = softMap.remove(key);
return bmpRef == null ? null : bmpRef.get();
}
@Override
public Collection<String> keys() {
synchronized (softMap) {
return new HashSet<String>(softMap.keySet());
}
}
@Override
public void clear() {
softMap.clear();
}
/** Creates {@linkplain Reference not strong} reference of value */
protected abstract Reference<Bitmap> createReference(Bitmap value);
}
/**
* Limited cache. Provides object storing. Size of all stored bitmaps will not to exceed size limit (
* {@link #getSizeLimit()}).<br />
* <br />
* <b>NOTE:</b> This cache uses strong and weak references for stored Bitmaps. Strong references - for limited count of
* Bitmaps (depends on cache size), weak references - for all other cached Bitmaps.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @see BaseMemoryCache
* @since 1.0.0
*/
public abstract class LimitedMemoryCache extends BaseMemoryCache {
private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16;
private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;
private final int sizeLimit;
private final AtomicInteger cacheSize;
/**
* Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed
* limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any
* time)
*/
private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
/** @param sizeLimit Maximum size for cache (in bytes) */
public LimitedMemoryCache(int sizeLimit) {
this.sizeLimit = sizeLimit;
cacheSize = new AtomicInteger();
if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB);
}
}
@Override
public boolean put(String key, Bitmap value) {
boolean putSuccessfully = false;
// Try to add value to hard cache
int valueSize = getSize(value);
int sizeLimit = getSizeLimit();
int curCacheSize = cacheSize.get();
if (valueSize < sizeLimit) {
while (curCacheSize + valueSize > sizeLimit) {
Bitmap removedValue = removeNext();
if (hardCache.remove(removedValue)) {
curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
}
}
hardCache.add(value);
cacheSize.addAndGet(valueSize);
putSuccessfully = true;
}
// Add value to soft cache
super.put(key, value);
return putSuccessfully;
}
@Override
public Bitmap remove(String key) {
Bitmap value = super.get(key);
if (value != null) {
if (hardCache.remove(value)) {
cacheSize.addAndGet(-getSize(value));
}
}
return super.remove(key);
}
@Override
public void clear() {
hardCache.clear();
cacheSize.set(0);
super.clear();
}
protected int getSizeLimit() {
return sizeLimit;
}
protected abstract int getSize(Bitmap value);
protected abstract Bitmap removeNext();
}
/**
* Limited {@link Bitmap bitmap} cache. Provides {@link Bitmap bitmaps} storing. Size of all stored bitmaps will not to
* exceed size limit. When cache reaches limit size then the bitmap which used the least frequently is deleted from
* cache.<br />
* <br />
* <b>NOTE:</b> This cache uses strong and weak references for stored Bitmaps. Strong references - for limited count of
* Bitmaps (depends on cache size), weak references - for all other cached Bitmaps.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.0.0
*/
public class UsingFreqLimitedMemoryCache extends LimitedMemoryCache {
/**
* Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache
* size will exceed limit then object with the least frequently usage is deleted (but it continue exist at
* {@link #softMap} and can be collected by GC at any time)
*/
private final Map<Bitmap, Integer> usingCounts =
Collections.synchronizedMap(new HashMap<Bitmap, Integer>());
public UsingFreqLimitedMemoryCache(int sizeLimit) {
super(sizeLimit);
}
@Override
public boolean put(String key, Bitmap value) {
if (super.put(key, value)) {
usingCounts.put(value, 0);
return true;
} else {
return false;
}
}
@Override
public Bitmap get(String key) {
Bitmap value = super.get(key);
// Increment usage count for value if value is contained in hardCahe
if (value != null) {
Integer usageCount = usingCounts.get(value);
if (usageCount != null) {
usingCounts.put(value, usageCount + 1);
}
}
return value;
}
@Override
public Bitmap remove(String key) {
Bitmap value = super.get(key);
if (value != null) {
usingCounts.remove(value);
}
return super.remove(key);
}
@Override
public void clear() {
usingCounts.clear();
super.clear();
}
@Override
protected int getSize(Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
protected Bitmap removeNext() {
Integer minUsageCount = null;
Bitmap leastUsedValue = null;
Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();
synchronized (usingCounts) {
for (Entry<Bitmap, Integer> entry : entries) {
if (leastUsedValue == null) {
leastUsedValue = entry.getKey();
minUsageCount = entry.getValue();
} else {
Integer lastValueUsage = entry.getValue();
if (lastValueUsage < minUsageCount) {
minUsageCount = lastValueUsage;
leastUsedValue = entry.getKey();
}
}
}
}
usingCounts.remove(leastUsedValue);
return leastUsedValue;
}
@Override
protected Reference<Bitmap> createReference(Bitmap value) {
return new WeakReference<Bitmap>(value);
}
}
來自:http://www.jianshu.com/p/e6f4e0ab57c3