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. 使用弱引用,即可以緩存正在使用的強引用資源,又不阻礙系統需要回收的無引用資源。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章