Glide學習(二)—緩存策略

前言

在前面一篇文章中,主要分析了Glide的工做流程,以加載網絡圖片爲例分析了Glide是如何工做的。在熟悉了Glide的工做流程後,咱們就能夠及繼續一些細節的分析。接下來,針對Glide的緩存策略進行分析。算法

咱們知道,一個高效的圖片框架是少不了緩存的,使用緩存能夠減小資源的重複加載,提升資源的利用率。在Glide中,緩存分爲兩大類:內存緩存以及硬盤緩存。具體到緩存類型能夠分爲4種,一下是官網給出的緩存類型。緩存

  • 活動資源 (Active Resources) - 如今是否有另外一個 View 正在展現這張圖片?
  • 內存緩存 (Memory cache) - 該圖片是否最近被加載過並仍存在於內存中?
  • 資源類型(Resource) - 該圖片是否以前曾被解碼、轉換並寫入過磁盤緩存?
  • 數據來源 (Data) - 構建這個圖片的資源是否以前曾被寫入過文件緩存?

能夠看到,內存緩存分爲活動資源以及內存資源;硬盤緩存分爲是否有處理過的資源以及原圖資源。接下來主要從緩存的更新以及如何緩存兩方面分析。網絡

Glide的緩存

緩存鍵

緩存鍵是查找的緩存的一個鍵值,在Glide中須要根據資源的信息構造緩存鍵,而後查找緩存資源。在分析Glide的工做流程時,在類Engine中開始加載資源時,咱們能夠看到構造緩存鍵。app

public <R> LoadStatus load(/**省略參數**/) {
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);
}
複製代碼

在Engine的load()方法中能夠看到在生成EngineKey的過程當中用到了不少參數,好比model資源途徑、signature簽名、寬高等。這多種參數共同決定了緩存鍵的生成。框架

內存緩存

上面說到內存緩存分爲活動資源和內存資源(這裏先將另一種資源稱爲內存資源)。其中活動資源是正在使用的圖片也就是正在View中展現的,內存資源是存在內存中的,沒有在使用。能夠看到,Glide在內存緩存這裏將資源又分爲了兩類。ide

我繼續從Engine的load()方法分析。這裏是真正開始加載資源的入口。函數

public <R> LoadStatus load(/**省略參數**/) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

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

    EngineResource<?> memoryResource;
    synchronized (this) {
      //從內存中加載資源
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(/**省略參數**/);
      }
    }
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }
複製代碼

從代碼裏能夠看到Glide先從內存中加載資源,這裏調用了loadFromMemory()方法。oop

private EngineResource<?> loadFromMemory(EngineKey key, boolean isMemoryCacheable, long startTime) {
    //這裏是一個配置,使用skipMemoryCache(boolean skip)能夠選擇是否跳過從緩存中獲取資源
    if (!isMemoryCacheable) {
      return null;
    }
    //從活動資源中取緩存
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      return active;
    }
    //從內存資源中取緩存
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      return cached;
    }

    return null;
  }
複製代碼

能夠看到,在上面的過程當中,Glide是先從活動資源取緩存,若是沒有相應的活動資源就從內存資源中取緩存。接着往下看。ui

private EngineResource<?> loadFromActiveResources(Key key) {
    EngineResource<?> active = activeResources.get(key); //取出活動資源
    if (active != null) {
      active.acquire();
    }
    return active;
  }
複製代碼

在代碼能夠看到是從activeResources取出的活動資源。咱們在這裏跟一下這變量能夠看到是在Engine的構造函數中進行的初始化。activeResources是ActiveResources類型的變量。咱們直接在類ActiveResources看接下來的邏輯。this

final class ActiveResources {
    @VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>(); //用來存儲活動資源
    /** * 將資源加入到活動資源中 */
    synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);

    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }

  /** * 將資源從活動資源中刪除 */
  synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if (removed != null) {
      removed.reset();
    }
  }

  /** * 獲取活動資源 */
  @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;
  }
}
複製代碼

這裏咱們只分析活動資源的增、刪、查三種操做,其餘一些細節能夠自行查看,好比ActiveResources在初始化的時候會清空隊列。

從代碼中能夠看到ActiveResources是經過一個Map容器存儲活動資源的,鍵就是EngineKey,資源以弱引用存儲。

  • 查:獲取資源的時候直接從Map中取出,而後從弱引用中拿到資源,若是資源爲null就回收資源。
  • 增:增長資源的時候直接插入到Map容器中,若是存在舊資源就回收舊資源。
  • 刪:刪除資源的時候直接從Map容器中刪除,而後回收資源。

以上就是Glide管理活動資源的一部分操做,在加載資源時,先從活動資源中取出。我接着往下看,內存資源是如何操做的。

private EngineResource<?> loadFromCache(Key key) {
    //獲取內存資源
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      //將內存資源加入到活動資源中
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
  }
複製代碼

上面的代碼作了兩個工做,第一步先從內存資源中取,而後再將內存資源加入到活動資源中。咱們接下來查看內存資源如何取出。

private EngineResource<?> getEngineResourceFromCache(Key key) {
    //從LruResourceCache中取出資源的同時將資源刪除
    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, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }
複製代碼

能夠看到內存資源是從一個cache的變量中取出的。我跟代碼能夠看到cache是一個MemoryCache類型的變量,繼續下去,能夠看到在Glide初始化的時候會初始化MemoryCache。

/**GlideBuilder**/
Glide build(@NonNull Context context) {
    if (diskCacheExecutor == null) {
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
    }
    
    if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }

    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }
    
    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }
}
複製代碼

在上面的代碼中,cache被初始化爲LruResourceCache類型。除此以外,還有其餘的一些資源被初始化,列出來的是硬盤緩存相關的,這裏先不說,接下來再講。

咱們繼續查看LruResourceCache,從類名上能夠看出內存資源是同LRU算法管理的。分析代碼能夠看到,LruResourceCache繼承自LruCache,具體邏輯也是在LruCache中實現,因此我分析LruCache的實現。

public class LruCache<T, Y> {
    private final Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
   
  /** * 獲取資源 */
  public synchronized Y get(@NonNull T key) {
    return cache.get(key);
  }
    
  /** * 增長資源 */
  public synchronized Y put(@NonNull T key, @Nullable Y item) {
    final int itemSize = getSize(item);
    if (itemSize >= maxSize) {
      onItemEvicted(key, item);
      return null;
    }

    if (item != null) {
      currentSize += itemSize;
    }
    @Nullable final Y old = cache.put(key, item);
    if (old != null) {
      currentSize -= getSize(old);

      if (!old.equals(item)) {
        onItemEvicted(key, old);
      }
    }
    evict();

    return old;
  }
  /** * 刪除資源 */
  public synchronized Y remove(@NonNull T key) {
    final Y value = cache.remove(key);
    if (value != null) {
      currentSize -= getSize(value);
    }
    return value;
  }
}
複製代碼

這裏也只分析內存資源的增、刪、查三種操做,其餘細節能夠自行查看。能夠看到LruCache直接使用了LinkedHashMap做爲容器存儲資源,也就意味着LruCache直接使用了LinkedHashMap的LRU算法。

  • 查:獲取資源很簡單,直接從容器中獲取。
  • 增:增長資源時,先判斷容器是否已滿,若是滿了會調用onItemEvicted()方法回收資源,不然返回查到的資源。
  • 刪:直接從容器中刪除。

上面的過程涉及到了內存資源的三個管理過程:

  1. 獲取資源時先從活動資源中獲取。
  2. 活動資源沒有就從內存資源中獲取。
  3. 從內存資源中獲取資源的同時將資源從活動資源中刪除並加入到活動資源中。

這三步能夠看到資源從內存資源到活動資源有個提高,可是內存緩存的資源時來自哪裏尚未分析,咱們接着往下將。內存緩存做爲第一級緩存,它的來源確定是從硬盤緩存或者網絡這些途徑。咱們從Glide工做流程中獲取完資源後開始資源回調的流程開始分析。

void notifyCallbacksOfResult() {
     // ......
    incrementPendingCallbacks(copy.size() + 1);
	//回調EngineJob完成
    engineJobListener.onEngineJobComplete(this, localKey, localResource);
	//......
    decrementPendingCallbacks();
  }
複製代碼

在上面的代碼中,資源完成後會回調onEngineJobComplete()方法,這個方式是Engine中的方法,

public synchronized void onEngineJobComplete( EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    if (resource != null && resource.isMemoryCacheable()) {
      //加入到活動資源
      activeResources.activate(key, resource);
    }
    jobs.removeIfCurrent(key, engineJob);
  }
複製代碼

在這個方法中就將獲取到的資源(可能來自於硬盤緩存或者網絡)加入到了活動資源中。除此以外還注意到其餘兩個函數incrementPendingCallbacks()以及decrementPendingCallbacks()。咱們看下他們作了什麼事情。

synchronized void incrementPendingCallbacks(int count) {
    Preconditions.checkArgument(isDone(), "Not yet complete!");
    if (pendingCallbacks.getAndAdd(count) == 0 && engineResource != null) {
      engineResource.acquire();
    }
  }
複製代碼
synchronized void decrementPendingCallbacks() {
    stateVerifier.throwIfRecycled();
    Preconditions.checkArgument(isDone(), "Not yet complete!");
    int decremented = pendingCallbacks.decrementAndGet();
    Preconditions.checkArgument(decremented >= 0, "Can't decrement below 0");
    if (decremented == 0) {
      if (engineResource != null) {
        engineResource.release();
      }
      release();
    }
  }
複製代碼

從代碼中能夠看到,在incrementPendingCallbacks()方法中對資源執行了engineResource.acquire()操做,這個操做的做用就至關於記錄資源的引用次數,同時記錄有多少個回調在使用資源。而在decrementPendingCallbacks()方法中能夠看到對回調數量執行了減操做,若是數量等於0,就釋放資源,咱們看下資源是如何釋放的。engineResource.release()執行了資源釋放的操做,咱們跟着代碼看下去,能夠發現最終調用了Engine的onResourceReleased()方法。

public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }
複製代碼

在這個方法中能夠看到,資源從活動資源中刪除,而後加入到了內存資源中。

到這裏Glide對內存緩存的管理就大體這些內容,包括:從內存緩存中去資源的過程以及內存緩存如何更新。

硬盤緩存

在分析Glide硬盤緩存的管理機制前,咱們先了解一下Glide中有哪些硬盤管理策略。

  • DiskCacheStrategy.NONE: 表示不緩存任何內容。
  • DiskCacheStrategy.SOURCE: 表示只緩存原始圖片。
  • DiskCacheStrategy.RESULT: 表示只緩存轉換事後的圖片(默認選項)。
  • DiskCacheStrategy.ALL : 表示既緩存原始圖片,也緩存轉換事後的圖片。
  • DiskCacheStrategy.AUTOMATIC:智能的選擇策略。

目前版本中Glide一共有這5中緩存策略。每種策略所對應的行爲都不相同,咱們下面會講到。

接下來,仍是從Glide的工做流程中開始講銀盤緩存策略。

上面說到,在加載資源時會先從內存緩存中取,若是內存緩存中不存在對應的資源,那麼加載流程會繼續進行下去,咱們直接分析。在Glide的工做流程中咱們知道經過DecodeJob中獲取各類類型的Generator加載資源。

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:
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }
複製代碼

能夠看到,在getNextStage()方法中首先會根據硬盤緩存策略獲取對應的Stage。首先根據decodeCachedResource()方法的結果選擇是否使用硬盤緩存。在上面已經介紹過了Glide不一樣的緩存策略,根據策略的不一樣decodeCachedResource()的返回結果不一樣。上面的5種策略中,NONE和DATA的返回值時false,NONE策略很好理解,由於它禁用了硬盤緩存。DATA策略的意思是直接獲取以前緩存的數據,而其餘的緩存策略會獲取數據對應的資源。只其中的差別接下來會說到。

如今咱們說下,在使用硬盤緩存狀況下的流程。那個getNextStage()方法會返回Stage.RESOURCE_CACHE。

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);
    }
  }
複製代碼

在getNextGenerator()能夠看到,Stage.RESOURCE_CACHE對應了ResourceCacheGenerator。咱們知道資源加載是在Generator的startNext()方法中執行的,咱們直接分析這個方法。

/**ResourceCacheGenerator**/
public boolean startNext() {
      //......
      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);
      //構造緩存鍵
      currentKey =
          new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      //獲取緩存資源
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }
	//......
    return started;
  }
複製代碼

能夠看到,首先構造了緩存鍵,而後再經過helper.getDiskCache().get(currentKey)獲取資源。我跟下這裏的代碼,能夠知道緩存是來自於DiskLruCacheWrapper。根據這個類名能夠知道硬盤緩存也是LRU算法管理的。

public File get(Key key) {
    String safeKey = safeKeyGenerator.getSafeKey(key);
    File result = null;
    try {
      //經過DiskLruCache獲取資源
      final DiskLruCache.Value value = getDiskCache().get(safeKey);
      if (value != null) {
        result = value.getFile(0);
      }
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.WARN)) {
        Log.w(TAG, "Unable to get from disk cache", e);
      }
    }
    return result;
  }
複製代碼

完後上面的過程後就取得了硬盤資源。

除此以外,咱們剛纔還說到了DATA類型的緩存策略,咱們看下這種策略的運行流程。上面說到DATA策略是直接獲取原有的數據,根據DATA策略的返回結果,getNextStage()方法返回的是Stage.DATA_CACHE,對應到Generator就是DataCacheGenerator。

/**DataCacheGenerator**/
public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
      sourceIdIndex++;
      if (sourceIdIndex >= cacheKeys.size()) {
        return false;
      }

      Key sourceId = cacheKeys.get(sourceIdIndex);
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      cacheFile = helper.getDiskCache().get(originalKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }
    //......
    return started;
  }
複製代碼

經過DataCacheGenerator的startNext()方法能夠看到,DATA策略也是先構造緩存鍵而後經過DiskLruCache獲取緩存的數據。

以上的就是硬盤緩存在不一樣的策略下的獲取過程,接下來咱們分析,何時將資源加入到硬盤緩存中。這個其實很容易想到在首次獲取資源時會將資源加入到硬盤緩存中。經過DiskLruCacheWrapper中put()方法的調用,咱們能夠看到在DecodeJob中會將資源加入到硬盤緩存中。

private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    //......
    //這個方法的回調會將資源加入到活動資源中
    notifyComplete(result, dataSource);
    stage = Stage.ENCODE;
    try {
      if (deferredEncodeManager.hasResourceToEncode()) {
        //這裏將資源加入到硬盤緩存中
        deferredEncodeManager.encode(diskCacheProvider, options);
      }
    }
    //......
  }
複製代碼
void encode(DiskCacheProvider diskCacheProvider, Options options) {
      try {
        diskCacheProvider
            .getDiskCache()
            .put(key, new DataCacheWriter<>(encoder, toEncode, options)); //將資源加入到硬盤緩存
      }
    //......
    }
複製代碼

能夠看到在以上資源回調的過程當中,Glide完成了內存緩存以及硬盤緩存的管理。

總結

到這裏Glide的緩存策略就大體講完了。上面的文章從緩存的獲取以及緩存的管理量方面分析了Glide的緩存原理。

從緩存獲取的角度來將,Glide在加載資源時:

  • 首先從活動資源中加載,若是沒有,進行下一步;
  • 從內存資源中獲取,若是存在,會將內存資源返回,並將資源加入到活動資源中;若是沒有,進行下一步;
  • 從硬盤緩存中獲取,硬盤緩存分爲兩種類型,存儲資源和資源數據。這兩種資源的加載原理是一致的。若是沒獲取到,就從其餘來源獲取,好比網絡資源。

從緩存管理來將:Glide管理的緩存分爲內存緩存和硬盤緩存。緩存算法都是使用的LRU算法。

內存緩存分爲活動資源和內存資源,活動資源的優先級高於內存資源。同時活動資源被回收時會將資源加入到內存資源中,在從內存資源中獲取到資源時會將資源加入到活動資源。

硬盤緩存根據緩存策略來不一樣表現不一樣,在資源首次獲取時會將資源加入到硬盤資源。

相關文章
相關標籤/搜索