Glide 缓存机制解析(为啥使用弱引用)

目前图片框架,基本就是 Glide 一统江山了,除了极其简单的链式调用,里面丰富的 API 也让人爱不释手。
那么,这样一个好用的框架,里面的缓存机制是怎么样的呢?
我们知道,一般图片框架,加载图片,都是通过内存缓存 LruCache ,DiskLruCache 硬盘缓存中去拿,那 Glide 又是怎么样的呢?这里,我们一起来探讨一下;

这里的 Glide 版本为 4.9.0

Glide 的缓存可以分为两种,一种内存缓存,一种是硬盘缓存;其中内存缓存又包含 弱引用 和 LruCache ;而硬盘缓存就是 DiskLruCache
流程图,可以参考这个,再去跟比较好:

在这里插入图片描述

一. 内存缓存

首先,Glide 的图片加载在 Engine 中的 load 方法中,如下:

public synchronized <R> LoadStatus load(
      
	// 获取资源的 key ,参数有8个
    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 (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
    }
	// 通过 LruCache
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return null;
    }

	// 如果都获取不到,则网络加载
	.... 

1.1 获取缓存key

可以看到,首先通过:

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

来获取 缓存的key,它的参数有8个,所以,当同一图片的,它的大小不一样时,也会生成一个新的缓存。

然后 Glide 默认是开启内存缓存的,如果你想关掉,可以使用:

//关闭内存缓存
skipMemoryCache(false) 

1.2 弱引用

接着,会从弱引用中是否拿到资源,通过:

EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);

里面的实现为:

  @Nullable
  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
    EngineResource<?> active = activeResources.get(key);
    //如果能拿到资源,则计数器 +1
    if (active != null) {
      active.acquire();
    }

    return active;
  }

#ActiveResources#get()
  @Nullable
  synchronized 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;
  }

#EngineResource#acquire()
  synchronized void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    ++acquired;
  }

可以看到,activeEngineResources 为实现了弱引用的 hasmap,通过 key 拿到弱引用的对象,如果获取不到,则可能GC,对象被回收,则从 map 中移除;如果拿到对象,则引用计数 acquired +1, 计数器后面再讲;

1.3 LruCache

如果获取不到,则通过

// 从 lrucache 获取对象
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);

#Engine#loadFromCache()
  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;
  }

我们看看 getEngineResourceFromCache 方法:

在这里插入图片描述
发现,它从 LruCache 那大对象,如果对象不为空,则通过 activeResources.activate(key, cached); 把它加入弱引用中,且从 LruCache 删除。且 调用 acquire() 让计数器 +1.

所以,我们知道了,Glide 的内存缓存的流程是这样的,先从弱引用中取对象,如果存在,引用计数+1,如果不存在,从 LruCache 取,如果存在,则引用计数+1,并把它存到弱引用中,且自身从 LruCache 移除。

上面,我们讲到的是取,那如果存呢?如果要对一个对象进行存储,那肯定在图片加载的时候去存。
回调 Engine 类的load 方法,其中通过加载的代码如下:

  public synchronized <R> LoadStatus load(
    ..
    // 获取 key
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);
 // 从 弱引用中获取对象
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    ...
 // 从 LruCache 获取对象
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
 ...
    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, callbackExecutor);
    engineJob.start(decodeJob);
 ...

这里两个关键的对象,一个是 EngineJob ,它是一个线程池,维护着编码、资源解析、网络下载等工作;一个是 DecodeJob ,它继承 Runnable ,相当于于 EngineJob 的一个任务;

engineJob.start(decodeJob); 可以知道,调用的是 DecodeJob 里面的 run 方法,具体细节,等硬盘缓存的时候,我们再跟;最后会回调EngineJob 的 onResourceReady 方法:

  @Override
  public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    synchronized (this) {
      this.resource = resource;
      this.dataSource = dataSource;
    }
    notifyCallbacksOfResult();
  }

#EngineJob #onResourceReady ()

  @Synthetic
  void notifyCallbacksOfResult() {
    ResourceCallbacksAndExecutors copy;
    Key localKey;
    EngineResource<?> localResource;
    synchronized (this) {
      stateVerifier.throwIfRecycled();
      //是否被取消
      if (isCancelled) {
        // TODO: Seems like we might as well put this in the memory cache instead of just recycling
        // it since we've gotten this far...
        resource.recycle();
        release();
        return;
      } else if (cbs.isEmpty()) {
        throw new IllegalStateException("Received a resource without any callbacks to notify");
      } else if (hasResource) {
        throw new IllegalStateException("Already have resource");
      }
      engineResource = engineResourceFactory.build(resource, isCacheable);
      // Hold on to resource for duration of our callbacks below so we don't recycle it in the
      // middle of notifying if it synchronously released by one of the callbacks. Acquire it under
      // a lock here so that any newly added callback that executes before the next locked section
      // below can't recycle the resource before we call the callbacks.
      hasResource = true;
      copy = cbs.copy();
      // 引用计数 +1
      incrementPendingCallbacks(copy.size() + 1);

      localKey = key;
      localResource = engineResource;
    }
    // 把对象put到弱引用上
    listener.onEngineJobComplete(this, localKey, localResource);

	// 遍历所有图片
    for (final ResourceCallbackAndExecutor entry : copy) {
    	// 把资源加载到 imageview 中,引用计数 +1
      entry.executor.execute(new CallResourceReady(entry.cb));
    }
    //引用计数 -1
    decrementPendingCallbacks();
  }

从上面看,notifyCallbacksOfResult() 方法做了以下事情

  1. 图片的引用计数 +1
  2. 通过 listener.onEngineJobComplete() ,它的回调为 Engine#onEngineJobComplete(),把资源 put 到 弱引用上,实现如下:

在这里插入图片描述

  1. 遍历加载的图片,如果加载成功,则引用计数+1,且通过 cb.onResourceReady(engineResource, dataSource) 回调给 target (imageview) 去加载
  2. 通过 decrementPendingCallbacks() 释放资源,引用计数 -1
  synchronized void decrementPendingCallbacks() {
   ....
    if (decremented == 0) {
      if (engineResource != null) {
      // 释放资源
        engineResource.release();
      }
      release();
    }
  }

#ResourceEnginer#release()
  void release() {
    // To avoid deadlock, always acquire the listener lock before our lock so that the locking
    // scheme is consistent (Engine -> EngineResource). Violating this order leads to deadlock
    // (b/123646037).
    synchronized (listener) {
      synchronized (this) {
        if (acquired <= 0) {
          throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
        }
        if (--acquired == 0) {
          listener.onResourceReleased(key, this);
        }
      }
    }
  }

当引用计数减到 0 时,即图片已经没有使用时,就会调用 onResourceReleased() 接口,它的实现如下:

  @Override
  public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
  // 从弱引用中移除
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
    //添加到 LruCache 中
      cache.put(cacheKey, resource);
    } else {
     // 回收资源
      resourceRecycler.recycle(resource);
    }
  }

这样,我们就知道了真个流程了,这里,我们再对流程梳理一下:

首先,从弱引用去缓存,如果有,则引用计数+1,没有则从 LruCache 取,如果有,则引用计数+1,且该缓存从 LruCache移除,存到 弱引用中。反过来,当该资源不再被引用时,就会从弱引用移除,存存到 LruCache 中。

而这个 引用计数是啥呢?acquired 这个变量是用来记录图片被引用的次数的,当从 loadFromActiveResources()、loadFromCache()、incrementPendingCallbacks,CallResourceReady#run 获取图片时,都会调用 acquire() 方法,让 acquired +1,当暂停请求或加载完毕,或清除资源都会调用 release() 方法,让 acquired -1

可以看到这个图:
在这里插入图片描述

二. 硬盘缓存

上面已经解释了 Glide 如何从内存缓存中拿到图片,但如果还是拿不到图片,则此时 Glide 会从以下两个方法来检查:

  1. 资源类型(Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存?
  2. 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?

即我们的硬盘缓存;Glide 的硬盘策略可以分为如下几种:

  • DiskCacheStrategy.RESOURCE :只缓存解码过的图片
  • DiskCacheStrategy.DATA :只缓存原始图片
  • DiskCacheStrategy.ALL : 即缓存原始图片,也缓存解码过的图片啊, 对于远程图片,缓存 DATA 和 RESOURCE;对本地使用 只缓存 RESOURCE。
  • DiskCacheStrategy.NONE :不使用硬盘缓存
  • DiskCacheStrategy.AUTOMATIC :默认策略,会对本地和和远程图片使用最佳的策略;对下载网络图片,使用 DATA,对于本地图片,使用 RESOURCE

**这里以下载一个远程图片为例子,且缓存策略为 DiskCacheStrategy.ALL **

在这里插入图片描述

从上面我们知道,一个图片的加载在 DecodeJob 这个类中,这个任务由 EngineJob 这个线程池去执行的。去到 run 方法,可以看到有个 runWrapped() 方法:
在这里插入图片描述
刚开始 runReason 初始化为 INITIALIZE , 所以它会走第一个 case,getNextStage 其实,就是对当前的缓存策略进行判断,由于我们的策略为DiskCacheStrategy.ALL ,所以 diskCacheStrategy.decodeCachedResource() 为true,即会解析解码的流程,所以 State 被赋值为 Stage.RESOURCE_CACHE,如下:

  private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

接着,调用 currentGenerator = getNextGenerator() 拿到当前的解码器为 ResourceCacheGenerator;然后 调用 runGenerators() 方法,它才是关键;它里面维护着一个 while 循环,即不断通过 startNext() 去解析不同的缓存策略,当 stage == Stage.SOURCE 的时候,才会退出。
在这里插入图片描述
接着会调用 ResourceCacheGenerator 的 startNext() 方法,它会从生成缓存key,从 DiskLruCache 拿缓存,如下:

 @Override
  public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
   ...
    while (modelLoaders == null || !hasNextModelLoader()) {
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        // 由于没有缓存,最后会在这里退出这个循环
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }

      Key sourceId = sourceIds.get(sourceIdIndex);
	//获取缓存 key
      currentKey =
          new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      // 尝试从 DiskLruCache 拿数据
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData = modelLoader.buildLoadData(cacheFile,
          helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

由于第一次肯定是拿不到缓存的,所以 while (modelLoaders == null || !hasNextModelLoader()) 循环会一直运行,直到返回 false。

 if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }

同理,接着来则是 DataCacheGenerator 也是同样,最后,当 stage == Stage.SOURCE 的时候,才会退出,并调用reschedule() 方法

而在 reschedule() 方法中,会把 runReason 的状态改成RunReason.SWITCH_TO_SOURCE_SERVICE ,并重新回调 run 方法

  @Override
  public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
    callback.reschedule(this);
  }

所以,它又会调用 runWrapped() 方法,但此时的 runReason 已经变成了 SWITCH_TO_SOURCE_SERVICE,所以它会执行 runGenerators() 方法
在这里插入图片描述
而在啊runGennerator() 方法中,它里面也是个 while 循环:

  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;
      }
    }
 ... 
  }

但此时的 currentGenerator 为 SourceGennerator ,已经不为null,所以,去到 SourceGennerator 的 startNext() 方法:

  @Override
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      // 存储到 DiskLruCache
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        //加载数据
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

首先它会判断 dataToCache 是否为 null,第一次肯定会 null,所以,可以先不管;这里看 loadData.fetcher.loadData(); 这个方法,loadData() 是个接口,它有很多个实现方法,由于我们这里假设是网络下载,所以去到 HttpUrlFetcher#loadData() 中:
在这里插入图片描述
可以看到,它拿到 inputStream 后,通过 onDataReady() 方法回调回去,在 SourceGenerator#onDataReady() 中,对 dataToCache 进行赋值;然后又调用 cb.reschedule(); 方法

  @Override
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }

可以看到,绕了一圈,又调用 cb.reschedule() 方法,所以,它还是会走 DecodeJob 的run方法,且执行 runWrapped();是不是看得很恶心?恩,我写得也是。
此时的 runReason 还是为 SWITCH_TO_SOURCE_SERVICE,currentGenerator 为 SourceGenerator ;所以,它还是会执行 SourceGenerator startNext() 方法,只不过此时 dataToCache 已经不为空,所以会执行 cacheData() 方法:
在这里插入图片描述
可以看到,对这个已经解析完的数据,通过 helper.getDiskCache().put() 方法,存到到 DiskLruCache 硬盘缓存中。并通过 loadData.fetcher.clearup() 清除任务,赋值 sourceCacheGenerator 为 DataCacheGenerator。
在这里插入图片描述
此时 sourceCacheGenerator 不为 null,所以会走 DataCacheGenerator 的startNext() 方法;

由于此时已经能从 DiskLruCache 拿到数据了,所以会跳出循环,走下一步:

在这里插入图片描述
然后则会调用 loadData.fetcher.loadData() 方法:
在这里插入图片描述
该方法会进入 MultiModeLoader#loadData 方法,里面才是重点;由于这种网络的,所以loadData () 方法中的 fetchers.get(currentIndex).loadData(),调用的是 ByteBufferFileLoader 方法:

    @Override
    public void loadData(
        @NonNull Priority priority, @NonNull DataCallback<? super Data> callback) {
      this.priority = priority;
      this.callback = callback;
      exceptions = throwableListPool.acquire();
      fetchers.get(currentIndex).loadData(priority, this);
	...
    }

#ByteBufferFileLoader
    @Override
    public void loadData(@NonNull Priority priority,
        @NonNull DataCallback<? super ByteBuffer> callback) {
      ByteBuffer result;
      try {
        result = ByteBufferUtil.fromFile(file);
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to obtain ByteBuffer for file", e);
        }
        callback.onLoadFailed(e);
        return;
      }
	// 回调 onDataReady 方法
      callback.onDataReady(result);
    }

#DataCacheGenerator#onDataReady
  @Override
  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
  }

可以看到,最后回调了DataCacheGenerator#onDataReady() 方法,接下来则是回到 EngineJob 的onDataFetcherReady() 方法了。
在这里插入图片描述
从调试可以看到,最后走了 decodeFromRetrievedData方法,然后走的方法为 EngineJob#notifyComplete() - EngineJob#onResourceReady()

可以看到,最后终于调用 EngineJob 的 onResourceReady() 方法了。
这个方法在内存缓存中已经分析,它会把资源存到 弱引用且加载图片等操作。

此致,Glide 的缓存机制我们就分析完了。

是不是记不住?记不住就对了,看下面的总结和流程图把。
总结:

硬盘缓存时通过在 EngineJob 中的 DecodeJob 中完成的,先通过ResourcesCacheGenerator、DataCacheGenerator 看是否能从 DiskLruCache 拿到数据,如果不能,从SourceGenerator去解析数据,并把数据存储到 DiskLruCache 中,后面通过 DataCacheGenerator 的 startNext() 去分发 fetcher 。
最后会回调 EngineJob 的 onResourceReady() 方法了,该方法会加载图片,并把数据存到弱引用中

流程图:
在这里插入图片描述

三. 为啥要用弱引用

我们知道,glide 是用弱引用缓存当前的活跃资源的;为啥不直接从 LruCache 取呢?原因猜测如下:

  1. 这样可以保护当前使用的资源不会被 LruCache 算法回收
  2. 使用弱引用,即可以缓存正在使用的强引用资源,又不阻碍系统需要回收的无引用资源。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章