转载请注明出处:
源码分析glide中三层存储机制并与常规三层缓存机制对比
地址:http://www.jianshu.com/p/dc8fcf7e69bc
目录
常规三层缓存机制
三级缓存的流程
强引用->软引用->硬盘缓存
当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找(软引用适合当cache,当内存吃紧的时候才会被回收。而weakReference在每次system.gc()就会被回收)(当LruCache存储紧张时,会把最近最少使用的数据放到SoftReference中),如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去硬盘缓存中中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到硬盘缓存中,然后放到LruCache中。(参考这里)
强引用
强引用不能是随便的一个map数组,因为还要处理最近不用的数据。一般用的是LruCache(这是内存存储,以前一直以为这是disk硬盘存储,囧,那个叫DiskLruCache),里面持有一个LinkedHashMap,需要将持有的对象进行按时间排序,这样才能实现Lru算法(Least Recently Used),也就是当达到最大的存储限制时,把最近最少使用的移除。使用Lrucache需要实现的最主要的方法(参考这里):
- entryRemoved(boolean evicted, String key, T oldValue, T newValue)这个作用是,当存储满了以后,需要移除一个最近不使用的。也就是这个oldValue,我们可以把这个oldValue存到本地或者变成弱引用。或者直接oldValue==null(因为LruCache只是把最近不使用的那个移出map,并没有置为null即没有移除内存,这个如果需要的话得自己处理)
- create(String key)这个就是当用户从内存中获取一个value的时候,没有找到,可以在这里生成一个,之后会放cache中。不过我们一般会用put进行放置,所以这里不实现也ok。
- int sizeOf(K key, V value)//这个方法要特别注意,跟我们实例化 LruCache 的 maxSize 要呼应,怎么做到呼应呢,比如 maxSize 的大小为缓存的个数,这里就是 return 1就 ok,如果是内存的大小,如果5M,这个就不能是个数 了,这是应该是每个缓存 value 的 size 大小,如果是 Bitmap,这应该是 bitmap.getByteCount();
软引用
当图片被LruCache移除的时候,我们需要手动将这张图片添加到软引用map(SoftReference)中,也就是在刚才提到的entryRemoved()中做这件事。我们需要在项目中维护一个由SoftReference组成的集合,其中存储被LruCache移除出来的图片。软引用的一个好处是当系统空间紧张的时候,软引用可以随时销毁(当然前提是这个内存对象除了软引用之外没有其他引用),因此软引用是不会影响系统运行的,换句话说,如果系统因为某个原因OOM了,那么这个原因肯定不是软引用引起的。例如:
private Map<String, SoftReference<Bitmap>> cacheMap=new HashMap<>();
@Override // 当有图片从LruCache中移除时,将其放进软引用集合中
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue != null) {
SoftReference<Bitmap> softReference = new SoftReference<Bitmap>(oldValue);
cacheMap.put(key, softReference);
}
glide中三层缓存机制
glide作为一个优秀的图片加载开源库,使用的就是三级存储机制。下面就详细说下(稍微和一般的三层缓存机制不一样):
三层存储的机制在Engine中实现的。先说下Engine是什么?Engine这一层负责加载时做管理内存缓存的逻辑。持有MemoryCache、Map<Key, WeakReference<EngineResource<?>>>。通过load()来加载图片,加载前后会做内存存储的逻辑。如果内存缓存中没有,那么才会使用EngineJob这一层来进行异步获取硬盘资源或网络资源。EngineJob类似一个异步线程或observable。Engine是一个全局唯一的,通过Glide.getEngine()来获取。
那么
在Engine类的load()方法前面有这么一段注释:
<p>
* The flow for any request is as follows:
* <ul>
* <li>Check the memory cache and provide the cached resource if present</li>
* <li>Check the current set of actively used resources and return the active resource if present</li>
* <li>Check the current set of in progress loads and add the cb to the in progress load if present</li>
* <li>Start a new load</li>
* </ul>
* </p>
*
Active resources are those that have been provided to at least one request and have not yet been released.
* Once all consumers of a resource have released that resource, the resource then goes to cache. If the
* resource is ever returned to a new consumer from cache, it is re-added to the active resources. If the
* resource is evicted from the cache, its resources are recycled and re-used if possible and the resource is
* discarded. There is no strict requirement that consumers release their resources so active resources are
* held weakly.
active resources存在的形式是弱引用:
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
MemoryCache是强引用的内存缓存,其实现类:
public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache
说到这里,我们具体说下Engine类的load()方法:
final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
这两句话是获取key,获取key会使用width,height,transform等,注意这里还会使用fetcher.getId()。这个和图片的url有关,可以自定义,所以可以进行动态url的存储。详细可以看这里.
接下来就是加载内存缓存了:
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}
@SuppressWarnings("unchecked")
private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key);
final EngineResource result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource) cached;
} else {
result = new EngineResource(cached, true /*isCacheable*/);
}
return result;
}
大致获取缓存图片的流程是:如果Lruche中有,那么根据key把对应的EngineResource取出,并从Lruche中删除。当EngineResource从Lruche删除时,会马上回调一个onResourceRemoved()接口方法,这个方法在Engine中实现:
@Override
public void onResourceRemoved(final Resource<?> resource) {
Util.assertMainThread();
resourceRecycler.recycle(resource);
}
在resourceRecycler.recycle(resource);中使用handler将回收的任务给到了主线程,即在主线程对资源进行回收。当然在资源回收时,需要进行判断,看是不是还有其他地方对资源进行了引用,如果有,那么就不进行回收;没有没有,那么再进行回收。看下EngineResource:
@Override
public void recycle() {
if (acquired > 0) {//是否还有引用
throw new IllegalStateException("Cannot recycle a resource while it is still acquired");
}
if (isRecycled) {
throw new IllegalStateException("Cannot recycle a resource that has already been recycled");
}
isRecycled = true;
resource.recycle();
}
从前面的loadFromCache()代码可以看出,当把资源从Lruche中取出以后,会执行:
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
资源引用会加1(因为现在的场景是获取的缓存图片会被使用,所以资源引用加1),并且把资源放到弱引用map中。所以这种情况下,前面的资源回收并不会执行。如果当需求清楚资源时(比如页面关闭,请求cancle),那么会调用EngineResource的release()方法将资源引用减1。如果引用变成0后,会调用一个onResourceReleased()接口方法,其在Engine中实现:
@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
会把资源从activeResources弱引用数组中清除,然后把资源放到Lruche中,然后将资源进行回收。
当资源异步加载成功后(网络/diskLrucache),除了会放到diskLrucache中(网络请求),资源还会放到哪里呢?
@SuppressWarnings("unchecked")
@Override
public void onEngineJobComplete(Key key, EngineResource<?> resource) {
Util.assertMainThread();
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
}
}
// TODO: should this check that the engine job is still current?
jobs.remove(key);
}
可以看到资源是放到activeResources中了。
我们总结一下:需要一个图片资源,假设Lruche中相应的资源图片,那么就就返回相应资源,同时从Lruche中清除,放到activeResources中。activeResources map是盛放正在使用的资源,以弱引用的形式存在。同时资源内部有被引用的记录。如果资源没有引用记录了,那么再放回Lruche中,同时从activeResources中清除。需要一个图片资源如果Lruche中没有,就从activeResources中找,找到后相应资源的引用加1。如果Lruche和activeResources中没有,那么进行资源异步请求(网络/diskLrucache),请求成功后,资源放到diskLrucache和activeResources中。
核心思想就是:使用一个弱引用map activeResources来盛放项目中正在使用的资源。Lruche中不含有正在使用的资源。资源内部有个计数器来显示自己是不是还有被引用的情况(当然这里说的被项目中使用/引用不包括被Lruche/activeResources引用)。把正在使用的资源和没有被使用的资源分开有什么好处呢?因为当Lruche需要移除一个缓存时,会调用resource.recycle()方法。注意到该方法上面注释写着只有没有任何consumer引用该资源的时候才可以调用这个方法。这也是为什么需要保证Lruche中的缓存资源都不能有外界的引用。那么为什么调用resource.recycle()方法需要保证该资源没有任何consumer引用呢?一开始我不是很理解,因为像bitmap.recycle(),即使bitmap还有资源引用,调用recycle()也没关系啊,大不了就是不会被gc罢了。后来发现glide中resource定义的recycle()和bitmap中的recycle()并不是一回事。glide中resource定义的recycle()要做的事情是把这个不用的资源(假设是bitmap或drawable)放到bitmapPool中。我们知道bitmapPool是一个bitmap回收再利用的库,在做transform的时候会从这个bitmapPool中拿一个bitmap进行再利用。这样就避免了重新创建bitmap,减少了内存的开支。而既然bitmapPool中的bitmap会被重复利用,那么肯定要保证回收该资源的时候(即调用资源的recycle()时),要保证该资源真的没有外界引用了。这也是为什么glide花费那么多逻辑来保证Lruche中的资源没有外界引用的原因。