源码分析glide中三层存储机制并与常规三层缓存机制对比

96
陈文超happylion
4.8 2017.10.27 18:52* 字数 2372

转载请注明出处:
源码分析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中的资源没有外界引用的原因。

原创技术博客