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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容