Android Glide4.0 源码遨游记(第五集——缓存机制)

Glide

 

前言

Android中加载图片的形式有很多种,网上也有很多的知名图片加载库,例如Glide、Picasso、Fresco等等,它们为我们带来的方便就不需再多言了,无论是从加载到缓存还是占位图等等都提供了简易的Api,且实现强大的功能。本系列只针对Glide4.0版本源码进行分析,提高自身阅读源码的能力,同时也是为了了解其中加载的流程以及缓存的原理,本文尽可能地截图说明结合源码解析,如有疏忽之处,还请指教。

关于作者

一个在奋斗路上的Android小生,欢迎关注,互相交流
GitHubGitHub-ZJYWidget
CSDN博客IT_ZJYANG
简 书Android小Y


 

前情回顾

前几集已经从Glide的最基本用法入手分析了Glide的请求、解析、加载图片的流程。深刻体会到Glide源码结构的复杂,但Glide作为一个优秀的图片加载框架,必然要在缓存上下点功夫,本文主要分析Glide的缓存机制(用过的都能体会到它的缓存给我们带来的丝滑体验,特别是在请求网络资源的场景,缓存机制尤为重要)

 

剧情(Glide 缓存 有备无患)

平时使用Glide做缓存相关的操作,主要有两个api,一个是是设置skipMemoryCache(boolean),表示是否开启内存缓存,另外一个是diskCacheStrategy(DiskCacheStrategy)表示是否开启硬盘缓存,如下:

Glide3.0以前的用法是:

Glide.with(this).load("http://xxx.xxx.png").skipMemoryCache(false).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);

Glide4.0的用法是:

RequestOptions requestOptions = new RequestOptions();
requestOptions.skipMemoryCache(false);
requestOptions.diskCacheStrategy(DiskCacheStrategy.NONE);
Glide.with(this).load("http://xxx.xxx.png").apply(requestOptions ).into(imageView);

调用方式有些差别,但核心缓存机制差不多,接下来的分析均以我们以4.0+为准,可以看到简单的一个设置,即可决定是否要开启缓存功能,那么Glide内部究竟是如何对图片做出"备份"的呢?

内存缓存

上一集 Android Glide4.0 源码遨游记(第四集)在讲Glide的into方法的时候,有提到一个关键的核心类:Engine,图片真正请求的地方正是从它的load方法开始的,而Glide也正是在这里做了核心的缓存操作,我们回头看看那个load方法的源码:

public <R> LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      Class<?> resourceClass,
      Class<R> transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map<Class<?>, Transformation<?>> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb) {
    Util.assertMainThread();
    long startTime = LogTime.getLogTime();

    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
    }

    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return null;
    }

    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    if (current != null) {
      current.addCallback(cb);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }

    EngineJob<R> engineJob =
        engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);

    DecodeJob<R> decodeJob =
        decodeJobFactory.build(
            glideContext,
            model,
            key,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            onlyRetrieveFromCache,
            options,
            engineJob);

    jobs.put(key, engineJob);

    engineJob.addCallback(cb);
    engineJob.start(decodeJob);

    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey("Started new load", startTime, key);
    }
    return new LoadStatus(cb, engineJob);
}

可以看到,在创建engineJob和decodeJob之前,Glide还做了一些手脚,先是通过

EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options);

创建了一个EngineKey对象,我们点进去buildKey看看:

class EngineKeyFactory {

  @SuppressWarnings("rawtypes")
  EngineKey buildKey(Object model, Key signature, int width, int height,
      Map<Class<?>, Transformation<?>> transformations, Class<?> resourceClass,
      Class<?> transcodeClass, Options options) {
    return new EngineKey(model, signature, width, height, transformations, resourceClass,
        transcodeClass, options);
  }
}

其实就是new了一个EngineKey对象,并把关于图片的很多加载信息和类型都传了进去,那这个Glide创建这个EngineKey意图何在?不急,我们暂且记住有这么个对象,继续往下看到这么一段:

EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
}

上文我们讲到,onResourceReady就是将资源回调给ImageView去加载,那么这里先是判断active不为空,然后就调用了onResourceReady并且return结束当前方法,那么这个active就很有可能是Glide对图片的一种缓存(暂且这样直观理解),可以看到刚才生成的key值也传了进去,所以我们看下loadFromActiveResources这个方法里做了什么:

@Nullable
  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
}

可以看到,先是判断isMemoryCacheable,这个就是我们skipMemoryCache传进来的值的相反值,即:

skipMemoryCache传true,isMemoryCacheable为false
skipMemoryCache传false,isMemoryCacheable为true

所以如果我们设置为不缓存,那么这个条件就会通过,也就是直接执行return null,那么刚才上一步的active对象也就相应地被赋为null,就会继续往下走。
如果设置了启用缓存,那么这个条件就不满足,继续往下,可以看到从activeResources中通过key去拿了个EngineResource,那么activeResources里面存的是什么数据呢?,我们看下ActiveResources类:

public class ActiveResources {
  //忽略部分代码

  final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

  @Nullable
  EngineResource<?> get(Key key) {
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }

    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }
}

可以看到实际上ActiveResrouces里是存了很多组key-resource的Map集合,并且resource是以弱引用的形式保存,然后get方法是根据一个key去map里面获取对应的资源对象,如果拿不到(即可能已经被系统GC回收),那就clear,返回获取到的EngineResource对象。

回到刚才Engine的load方法中,可以看到在调用loadFromActiveResources获取不到的情况下,会调用loadFromCache来获取,那么这个loadFromCache又是从什么地方获取数据呢?loadFromCahce源码如下:

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
}

同样是判断isMemoryCacheable,道理同上,就不再复述了,接着它调用了getEngineResourceFromCache,跟进去看看:

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 /*isMemoryCacheable*/, true /*isRecyclable*/);
    }
    return result;
}

可以看到,这里先是调用了一个cache对象的remove,cache是一个MemoryCache接口对象,看下Glide对MemoryCache的定义:

MemoryCache声明

可以看到是一个从内存中添加和移除资源的操作接口,看下它的实现类LruResourceCache

LruResourceCache

看到继承了LruCache,也就是说,cache实际上是一个根据LruCache算法缓存图片资源的类,刚才把EngineResource从其中remove掉,并且返回了这个被remove掉的EngineResource对象,如果为null,说明Lru缓存中已经没有该对象,如果不为null,则将其返回。
所以getEngineResourceFromCache其实就是根据Lru算法从内存中获取图片资源,并且获取到的话就顺便从LruCache中移除掉,接着看回刚才的loadFromCache方法:

loadFromCache

可以看到,假如刚才从LruResourceCache中拿到了缓存,那么就会调用EngineResource的acquire()方法:

void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    if (!Looper.getMainLooper().equals(Looper.myLooper())) {
      throw new IllegalThreadStateException("Must call acquire on the main thread");
    }
    ++acquired;
}

这里只是将acquired+1,那么acquired变量用来干嘛的呢?我们跟踪它被引用的地方,可以看到EngineResource的release()

void release() {
    if (acquired <= 0) {
      throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
    }
    if (!Looper.getMainLooper().equals(Looper.myLooper())) {
      throw new IllegalThreadStateException("Must call release on the main thread");
    }
    if (--acquired == 0) {
      listener.onResourceReleased(key, this);
    }
}

可以看到,每次调用release的时候(比如暂停请求或者加载完毕时,这里就不展开讲了,跟踪一下即可),会将acquired-1,并且当acquired为0的时候,会调用listener的onResourceReleased方法,而这个listener正是Engine,我们回到Engine中看它的实现:

@Override
  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    Util.assertMainThread();
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
}

可以看到调用了activeResources的deactivate方法(这个方法作用就是将该资源对象从弱引用集合activeResources中移除),接着,可以看到再将其put进cache,cache我们刚才提到了,是一个基于Lru算法的缓存管理类,所以这里就是将其加进了LruCache缓存中。

也就是说,每次触发release就会将acquired变量-1,一旦它为0时,就会触发Engine将该缓存从弱引用中移除,并且加进了LruCache缓存中。换句话理解就是,这个资源暂时不用到,Glide把它从弱引用转移到了LruCache中

而刚才的loadFromCache方法里,调用了acquire()使得 acquired+1,也就是此刻我正要使用这个缓存,做个标记,这样就不会被转移到LruCache中。

联合起来的逻辑就是:先从LruCache中拿缓存,如果拿到了,就从LruCache缓存转移到弱应用缓存池中,并标记一下此刻正在引用。反过来,一旦没有地方正在使用这个资源,就会将其从弱引用中转移到LruCache缓存池中

 

硬盘缓存

分析完了内存缓存,我们再来看看Glide的硬盘缓存。依然是接着刚才最开始的Engineload方法,在判断了两级内存缓存之后,如果拿不到缓存,就会接着创建EngineJobDecodeJob(这两个的作用见我另外一篇文章Android Glide4.0 源码遨游记(第四集) ),然后接着就会调用进DecodeJob线程的run方法:

@Override
public void run() {
    TraceCompat.beginSection("DecodeJob#run");
    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();
    } catch (Throwable t) {
      if (stage != Stage.ENCODE) {
        throwables.add(t);
        notifyFailed();
      }
      if (!isCancelled) {
        throw t;
      }
    } finally {
      if (localFetcher != null) {
        localFetcher.cleanup();
      }
      TraceCompat.endSection();
    }
  }

  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
}

run中主要还是调用的runWrapper方法,继而调用runGenerator:

private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
    // We've run out of stages and generators, give up.
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
      notifyFailed();
    }

    // Otherwise a generator started a new load and we expect to be called back in
    // onDataFetcherReady.
}

这里调用了一个循环获取解析生成器Generator的方法,而解析生成器有多个实现类:ResourcesCacheGenerator、SourceGenerator、DataCacheGenerator,它们负责各种硬盘缓存策略下的缓存管理,所以这里关键的条件在于currentGenerator.startNext()循环获取每个Generator能否获取到缓存,获取不到就通过getNextGenerator进行下一种:

private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
}

所以我们看看ResourceCacheGenerator的startNext,看下它是用什么来缓存的,其中部分代码如下:

currentKey =
          new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
cacheFile = helper.getDiskCache().get(currentKey);

这里通过一个资源的关键信息生成key,然后调用helper.getDiskCache().get(),我们跟进去DiskCache看看:

DiskCache getDiskCache() {
    return diskCacheProvider.getDiskCache();
}
interface DiskCacheProvider {
    DiskCache getDiskCache();
}

可以看到最终是调用了DiskCacheProvider接口的getDiskCache方法获取一个DiskCache对象,那么这个D对象又是什么来头呢?


DiskCache

可以看到这是一个用来缓存硬盘数据的接口,那么它的实现就是我们要找的最终目标:


DiskLruCache.png

里面的就不详细分析下去了,这里主要维护了一个DiskLruCache,Glide就是通过这个来实现硬盘缓存的。

可以看到Glide的硬盘缓存是依靠DiskLruCache来进行缓存的,同样也是Lru算法。

总结

Glide4.0的缓存机制概况来说就是,先从弱引用缓存中获取数据,假如获取不到,就再尝试从LruCache中获取数据,假如从LruCache中获取到数据的话,就会将其从LruCache中转移到弱引用缓存中,这么做的优点是,下次再来拿数据的时候,就可以直接从弱引用中获取。
资源对象用一个acquired变量用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1,然后一旦acquired减为0(没有地方引用该资源),就会将其从弱引用中移除,添加到LruCache中。
使用中的资源会用弱引用来缓存,不在使用的资源会添加到LruCache中来缓存。
在二者都获取不到的情况下会根据硬盘缓存策略通过DiskLruCache去硬盘中获取数据,正是这样优秀的缓存机制,让我们在没有网络的情况下也能有很好的体验。

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