Android 【手撕Glide】--Glide緩存機制

本文源碼解析基于Glide 4.6.1
不知道大家最開始使用Glide的原因是什么霎冯?我的原因很簡單就是沖著那句Glide.with(this).load(url).into(imageview)去的,再加上Google的推薦啃炸,就一直沿用至今畦娄。以前也不太了解它,就知道它使用簡潔而且很火熟尉,不過最近看了一些它的源碼設計归露,算是找到了使用Glide理由。我目前的緣由如下:
1斤儿、Glide通過高度封裝之后剧包,通過外觀模式對外提供了非常簡潔的API調用,貌似外觀模式的很多庫都很受歡迎雇毫;
2玄捕、Glide自動感知生命周期,很節(jié)約資源棚放,不會內存泄漏枚粘;
3、超級強大的緩存機制飘蚯;
4馍迄、各種圖片轉換,超級方便局骤。

Android 【手撕Glide】--Glide緩存機制
Android 【手撕Glide】--Glide緩存機制(面試)
Android 【手撕Glide】--Glide是如何關聯(lián)生命周期的攀圈?

我想只要用過Glide的同學都或多或少聽過Glide的緩存機制,比如Glide用了3級緩存峦甩;又用了Lrucache赘来、DiskLrucache;Glide緩存圖片會緩存多張等等凯傲。但還是有很多同學對緩存源碼和緩存原理沒有一個整體的清晰的思路犬辰,本文就是來解決這個問題的,為了高效的學習冰单,本文按照如下思路來講解Glide緩存機制:

  • Glide緩存簡介
  • Glide緩存Key
  • Glide內存緩存的讀寫
  • Glide磁盤緩存的讀寫

Glide緩存簡介

三級緩存or二級緩存幌缝?
在沒學習源碼之前,我連這個最基本的概念都不確定诫欠,以前老是聽人說緩存是內存--->磁盤--->網絡這樣的方式去獲取圖片資源的涵卵,但這就是3級緩存嗎浴栽?明顯不是,這個只是2級緩存轿偎;Glide也是按照這種方式獲取圖片的典鸡,但是略有不同,Glide將它的緩存分為2個大的部分贴硫,一個是內存緩存椿每,一個是硬盤緩存。其中內存緩存又分為2種英遭,弱引用和Lrucache;磁盤緩存就是DiskLrucache亦渗,DiskLrucache算法和Lrucache差不多的挖诸,所以現(xiàn)在看起來Glide3級緩存的話應該是WeakReference + Lrucache + DiskLrucache

內存緩存的主要作用是防止應用重復將圖片數據讀取到內存當中法精;而硬盤緩存的主要作用是防止應用重復從網絡或其他地方下載和讀取數據多律。

Glide緩存Key

緩存是為了解決重復加載問題,那必然要有一個key來區(qū)分不同的圖片資源搂蜓。從下面生成key的代碼可以看出Glide生成key的方式遠比我們想象的要復雜狼荞,決定緩存Key的參數有8種,其中包括圖片URL帮碰、寬相味、高。

#Engine.load()
//生成緩存key
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

這里可以得出一個結論殉挽,幾乎任意配置的改變都會導致同一張圖片生成多個緩存key丰涉。舉個例子:同一張圖片加載到2個不同大小的ImageView會生成2個緩存圖片。至于EngineKey的作用斯碌,當然是用于讀取/寫入緩存圖片的時候用到的,別著急夕春,后面的流程你會多次看到的惶我。

Glide內存緩存的讀寫

這里先從內存緩存說起吧,首先Glide默認開啟了內存緩存冠骄,當然你可以選擇手動關閉伪煤。注意:只有開啟了內存才能使用下面的內存緩存功能。

skipMemoryCache(true) //關閉內存緩存

前面提到過內存緩存是通過弱引用+LruCache的方式實現(xiàn)的猴抹。那內存緩存在哪里實現(xiàn)的呢带族?還記得剛才在Engine#load()方法中生成緩存Key嗎?內存緩存的代碼也在這里實現(xiàn)的蟀给,下面一起看一下源碼:

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();

    //1.生成緩存key
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    //2.從弱引用讀取內存緩存
    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;
    }

    //3.從LruCache讀取緩存
    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;
    }
 
    //...省略
    //4.通過線程池從網絡加載圖片
   

看一下內存緩存部分的邏輯蝙砌,首先通過loadFromActiveResources從弱引用讀妊舳椤;如果沒有再通過loadFromCache從LruCache讀仍窨恕恬总;2者中的任意一個獲取到數據就會調用onResourceReady就是將資源回調給ImageView去加載。

Engine#loadFromActiveResources():從弱引用讀取緩存

public class Engine  {
  private final ActiveResources activeResources;

private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
//沒有開啟內存緩存就直接返回   
 if (!isMemoryCacheable) {
      return null;
    }
    //弱引用獲取緩存圖片
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
  }
}

#ActiveResources#get()
class ActiveResources {
    //弱引用的hashmap
    Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

    EngineResource<?> get(Key key) {
    //1.從弱引用的map獲取圖片資源
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }
    //2.最終需要的資源對象
    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }
}

這里的邏輯也不復雜肚邢,通過弱引用的hashmap來存儲資源壹堰,Key是緩存key,ResourceWeakReference代表資源骡湖,它繼承WeakReference贱纠。首先從弱引用的map獲取圖片資源,然后通過弱引用的get()方法獲取最終需要的對象响蕴,如果activeRef.get();拿不到(可能已經被系統(tǒng)GC回收)谆焊,那就clear(從弱引用中移除,清除資源等)浦夷。

再回到剛才Engine的load方法中辖试,如果loadFromActiveResources獲取不到,會調用loadFromCache來獲取劈狐。

Engine#loadFromCache():從LruCache讀取緩存

public class Engine  {
  private final ActiveResources activeResources;
 //Lrucache對象
    private final MemoryCache cache;

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
    //從lrucache獲取
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      //存到弱引用的HashMap
      activeResources.activate(key, cached);
    }
    return cached;
  }

private EngineResource<?> getEngineResourceFromCache(Key key) {
    //從lrucache刪除資源
    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;
  }
}

邏輯比較簡單罐孝,通過lrucache獲取圖片資源,如果獲取到的話就會從LruCache中刪除這張圖片肥缔,然后會調用acquire()方法和activate()方法,其中activate()是把取到的數據會存到弱引用中莲兢,說白了就是把圖片從LruCache轉移到弱引用

EngineResource# acquire()

class EngineResource<Z> implements Resource<Z> {
  //圖片引用計數器 
 private int acquired;

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是用一個acquired變量用來記錄圖片被引用的次數姑宽,調用acquire()方法會讓變量加1遣耍,調用release()方法會讓變量減1,release()方法后面調用的時候會講到炮车。舵变。

到這里,內存緩存的讀取就說完了瘦穆,下面講一下內存緩存的寫入纪隙。很明顯緩存的寫入是在加載圖片之后,所以回到剛才Engine#load()方法

 public <R> LoadStatus load(//一系列參數) {

  //...省略

  //1.從弱引用讀取內存緩存
   loadFromActiveResources()

    //2.從LruCache讀取緩存
 loadFromCache();

    //3.通過EngineJob加載圖片

    EngineJob<R> engineJob =
        engineJobFactory.build();

    DecodeJob<R> decodeJob =
        decodeJobFactory.build();

    jobs.put(key, engineJob);

    engineJob.addCallback(cb);
    //通過線程池加載圖片
    engineJob.start(decodeJob);

  }

這里有2個關鍵的對象扛或,EngineJob和DecodeJob绵咱,EngineJob 內部維護了線程池,用來管理資源加載熙兔,當資源加載完畢的時候通知回調悲伶; DecodeJob 是線程池中的一個任務艾恼。最后通過start()方法加載圖片,實際上是在DecodeJobrun()方法中完成的,當圖片加載完成麸锉,最終會回調EngineJob#onResourceReady ()

#EngineJob
public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    this.resource = resource;
    this.dataSource = dataSource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
  }

@Override
public boolean handleMessage(Message message) {
  EngineJob<?> job = (EngineJob<?>) message.obj;
  switch (message.what) {
    case MSG_COMPLETE:
      job.handleResultOnMainThread();
      break;
    case MSG_EXCEPTION:
      job.handleExceptionOnMainThread();
      break;
    case MSG_CANCELLED:
      job.handleCancelledOnMainThread();
      break;
    default:
      throw new IllegalStateException("Unrecognized message: " + message.what);
  }
  return true;
}

EngineJob#handleResultOnMainThread ()

void handleResultOnMainThread() {
    stateVerifier.throwIfRecycled();
    if (isCancelled) {
      resource.recycle();
      release(false /*isRemovedFromQueue*/);
      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);
    hasResource = true;

    // Hold on to resource for duration of request so we don't recycle it in the middle of
    // notifying if it synchronously released by one of the callbacks.
    //1.圖片引用計數器+1    
    engineResource.acquire();
    //2.回調到EngineJob處理
    listener.onEngineJobComplete(this, key, engineResource);

    //noinspection ForLoopReplaceableByForEach to improve perf
    //3.遍歷加載的圖片
    for (int i = 0, size = cbs.size(); i < size; i++) {
      ResourceCallback cb = cbs.get(i);
      if (!isInIgnoredCallbacks(cb)) {
        //圖片引用計數器+1    
        engineResource.acquire();
        //將資源回調給ImageView去加載
        cb.onResourceReady(engineResource, dataSource);
      }
    }
    // Our request is complete, so we can release the resource.
    //4.釋放資源钠绍,圖片引用計數器-1  
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }

這里一共有4步:
1、圖片引用計數器+1花沉;
2柳爽、listener.onEngineJobComplete(),這個listener是EngineJobListener接口對象碱屁,這里是將結果回調給Engine#onEngineJobComplete()處理磷脯;
3、遍歷遍歷加載的圖片娩脾,每加載到一張圖片争拐,引用計數器+1 ,并且會將資源回調給ImageView去加載晦雨;
4、釋放資源隘冲,圖片引用計數器-1 闹瞧。

public void onEngineJobComplete(EngineJob<?> engineJob, 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.activate(key, resource);
      }
    }

    jobs.removeIfCurrent(key, engineJob);
  }

可以看到,這里把資源放到弱引用展辞,也就是內存緩存的寫入了奥邮。但是LruCache緩存貌似還沒有出現(xiàn),再回頭看看剛才的Engine#onEngineJobComplete()方法罗珍,最后還調用了還調用了 engineResource.release()方法來釋放資源洽腺,還記得之前講過這個方法嗎,在獲取內存緩存的時候會調用acquire()覆旱,使得acquired+1蘸朋;而調用release()方法會讓acquired -1。

EngineResource#release()

class EngineResource<Z> implements Resource<Z> {
  //圖片引用計數器 
 private int acquired;

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()的時機主要是:加載網絡圖片時暫停請求/加載完畢以及清除資源扣唱。release()將acquired-1藕坯,并且當acquired==0的時候,會調用listener.onResourceReleased()方法噪沙,而這個listener正是Engine炼彪。

Engine#onResourceReleased()

public class Engine implements EngineJobListener,
    MemoryCache.ResourceRemovedListener,
    EngineResource.ResourceListener {

@Override
  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    Util.assertMainThread();
    //從弱引用集合activeResources中移除資源
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
      //放入LruCache緩存
      cache.put(cacheKey, resource);
    } else {
      //回收資源
      resourceRecycler.recycle(resource);
    }
  }
}

這個onResourceReleased ()方法也不復雜,作用是釋放資源正歼,先從弱引用集合activeResources中移除資源辐马,然后再把圖片資源放入LruCache緩存。

注意:在上面的調用EngineJob#handleResultOnMainThread ()去加載圖片等時候局义,如果加載圖片成功喜爷,那么acquired>=1冗疮,說明有圖片正在被引用;而等到暫停請求/退出頁面的時候再次調用release()時贞奋,acquired==0才會去調用onResourceReleased ()把緩存從弱引用轉移到Lrucache赌厅。

小結

這個acquired變量是用來記錄圖片被引用的次數,調用acquire()方法會讓變量加1轿塔,調用release()方法會讓變量減1特愿。當調用loadFromActiveResources()loadFromCache()勾缭、EngineJob#handleResultOnMainThread()獲取圖片的時候都會執(zhí)行acquire()方法揍障;當暫停請求或者加載完畢或者清除資源時會調用release()方法。

注意:從弱引用取緩存俩由,拿到的話毒嫡,引用計數+1;從LruCache中拿緩存幻梯,拿到的話兜畸,引用計數也是+1,同時把LruCache緩存轉移到弱應用緩存池中碘梢;從EngineJob去加載圖片咬摇,拿到的話,引用計數也是+1煞躬,會把圖片放到弱引用肛鹏。反過來,一旦沒有地方正在使用這個資源恩沛,就會將其從弱引用中轉移到LruCache緩存池中在扰。這也說明了正在使用中的圖片使用弱引用來進行緩存,暫時不用的圖片使用LruCache來進行緩存的功能雷客。


Glide磁盤緩存的讀寫

Glide5大磁盤緩存策略
DiskCacheStrategy.DATA: 只緩存原始圖片芒珠;
DiskCacheStrategy.RESOURCE:只緩存轉換過后的圖片;
DiskCacheStrategy.ALL:既緩存原始圖片佛纫,也緩存轉換過后的圖片妓局;對于遠程圖片,緩存 DATARESOURCE呈宇;對于本地圖片好爬,只緩存 RESOURCE
DiskCacheStrategy.NONE:不緩存任何內容甥啄;
DiskCacheStrategy.AUTOMATIC:默認策略存炮,嘗試對本地和遠程圖片使用最佳的策略。當下載網絡圖片時,使用DATA穆桂;對于本地圖片宫盔,使用RESOURCE

上面講內存緩存寫入的時候說到過享完,如果在內存緩存中沒獲取到數據灼芭,就通過DecodeJobEngineJob加載圖片。EngineJob 內部維護了線程池般又,用來管理資源加載彼绷,當資源加載完畢的時候通知回調; DecodeJob 是線程池中的一個任務茴迁。

DecodeJob#run()

public void run() {
   ...
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();
    } 
    ...
  }

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

這里會執(zhí)行到runWrapper()方法寄悯,對于一個新的任務,會執(zhí)行第一個分支堕义。stage:用來決定 DecodeJob 狀態(tài)猜旬,表示數據的加載狀態(tài);currentGenerator:是解析生成器倦卖,有多個實現(xiàn)類:ResourcesCacheGenerator洒擦、SourceGeneratorDataCacheGenerator怕膛,它們負責各種硬盤緩存策略下的緩存管理:

  • ResourceCacheGenerator:管理變換之后的緩存數據秘遏;
  • SourceGenerator:管理未經轉換的原始緩存數據;
  • SourceGenerator:直接從網絡下載解析數據嘉竟。

接下來會調用3個方法getNextStage getNextGenerator runGenerators()

#DecodeJob
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);
    }
  }

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

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

別怕啊洋侨,寫到這里我也很惡心了舍扰,本來想要一筆帶過的,但是發(fā)現(xiàn)網上沒什么博客能說清楚這個問題,還是寫一下吧希坚。這個地方之所以這么麻煩边苹,是因為緩存策略的原因

還是回到上面的runWrapped()方法裁僧,它先是調用了getNextStage(Stage.INITIALIZE),于是進入getNextStage()第一個分支个束,根據緩存策略返回Stage,由于我用的是默認的緩存策略聊疲,這里decodeCachedResource返回true茬底,于是getNextStage()方法返回Stage.RESOURCE_CACHE;然后執(zhí)行getNextGenerator方法获洲,根據上一步的stage阱表,這里執(zhí)行第一個分支,返回ResourceCacheGenerator,這個方法返回的3個Generator對象都是用于加載圖片資源的;接著調用runGenerators()方法:它通過while循環(huán)來獲取那3個解析生成器Generator最爬,循環(huán)條件主要是currentGenerator.startNext(),它的實際調用在那3個Generator里面涉馁,在方法內部又會獲取stage和currentGenerator,當stage == Stage.SOURCE時會跳出循環(huán)爱致。如果是第一次從網絡加載圖片的話烤送,最終數據的加載會交給 SourceGenerator 進行;如果是從磁盤緩存獲取的話會根據緩存策略的不同從ResourceCacheGenerator或者DataCacheGenerator獲取

先來看一下ResourceCacheGeneratorstartNext()方法糠悯,果不其然帮坚,里面先是構建了緩存key,然后從DiskLruCache獲取到了緩存圖片逢防。

#ResourceCacheGenerator
  public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
    if (sourceIds.isEmpty()) {
      return false;
    }
    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
    if (resourceClasses.isEmpty()) {
      if (File.class.equals(helper.getTranscodeClass())) {
        return false;
      }
    }
    while (modelLoaders == null || !hasNextModelLoader()) {
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }

      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);
      currentKey =
          new ResourceCacheKey( // 1 構建獲取緩存信息的鍵
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      cacheFile = helper.getDiskCache().get(currentKey); // 2 從緩存中獲取緩存信息
      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++); // 3 使用文件方式從緩存中讀取緩存數據
      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;
  }

這里是通過ResourceCacheGenerator獲取的緩存圖片叶沛,其實DataCacheGenerator也是差不多的。到這里磁盤緩存讀取就說完了忘朝,下面來看一下磁盤緩存的寫入灰署。不用想,是第一次從網絡加載圖片時寫入的局嘁,第一次從網絡加載時溉箕,會調用SourceGenerator#startNext()方法:

#SourceGenerator
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;
        //DataFetcher加載數據
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

這里用的是SourceGeneratorstartNext()方法,其它2個Generator的實現(xiàn)是不一樣的悦昵。先判斷是否有可以用于緩存的數據肴茄,由于是第一次加載網絡圖片,所以是沒有的但指,然后通過DataFetcher加載數據寡痰,具體來說網絡的話是通過HttpUrlFetcher來實現(xiàn)的。

HttpUrlFetcher#loadData()

public void loadData(@NonNull Priority priority,
      @NonNull DataCallback<? super InputStream> callback) {
    long startTime = LogTime.getLogTime();
    try {
      //加載數據獲得文件輸入流
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
      //在 SourceGenerator 回調
      callback.onDataReady(result);
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Failed to load data for url", e);
      }
      callback.onLoadFailed(e);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

通過HttpURLConnection來獲取InputStream棋凳,然后在 SourceGenerator 回調拦坠。

SourceGenerator #onDataReady ()

#SourceGenerator
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);
    }
  }

這里先是保存數據,然后回調到DecodeJob中剩岳, 將會根據當前的stage從 run() 方法開始執(zhí)行一遍贞滨,并再次調用SourceGeneratorstartNext() 方法。這次已經存在可以用于緩存的數據了拍棕。所以cacheData()方法將會被觸發(fā):

private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      //DiskLrucache保存圖片
      helper.getDiskCache().put(originalKey, writer);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished encoding source to cache"
            + ", key: " + originalKey
            + ", data: " + dataToCache
            + ", encoder: " + encoder
            + ", duration: " + LogTime.getElapsedMillis(startTime));
      }
    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }

這里通過DiskCache對象晓铆,實際上是DiskLruCacheWrapper實現(xiàn)類對象在磁盤緩存圖片。

小結
磁盤緩存是在EngineJob中的DecodeJob任務中完成的绰播,依次通過ResourcesCacheGenerator骄噪、SourceGeneratorDataCacheGenerator來獲取緩存數據蠢箩。ResourcesCacheGenerator獲取的是轉換過的緩存數據腰池;SourceGenerator獲取的是未經轉換的原始的緩存數據尾组;DataCacheGenerator是通過網絡獲取圖片數據再按照按照緩存策略的不同去緩存不同的圖片到磁盤上。

到這里示弓,總算寫完了整體流程分析讳侨。不過一大堆,你基本不太可能記得住奏属,下面就給大家總結一下吧:

總結(干貨)

Glide緩存分為弱引用+ LruCache+ DiskLruCache跨跨,其中讀取數據的順序是:弱引用 > LruCache > DiskLruCache>網絡;寫入緩存的順序是:網絡 --> DiskLruCache-->弱引用-->LruCache

內存緩存分為弱引用的和 LruCache 囱皿,其中正在使用的圖片使用弱引用緩存勇婴,暫時不使用的圖片用 LruCache緩存,這一點是通過 圖片引用計數器(acquired變量)來實現(xiàn)的嘱腥,詳情可以看內存緩存的小結耕渴。

磁盤緩存就是通過DiskLruCache實現(xiàn)的,根據緩存策略的不同會獲取到不同類型的緩存圖片齿兔。它的邏輯是:先從轉換后的緩存中瘸髁场;沒有的話再從原始的(沒有轉換過的)緩存中拿數據分苇;再沒有的話就從網絡加載圖片數據添诉,獲取到數據之后,再依次緩存到磁盤和弱引用医寿。

拋一個問題給大家思考討論:
為什么Glide內存緩存要設計2層栏赴,弱引用和LruCache?

這是一個朋友的理解 用弱引用緩存的資源都是當前活躍資源 activeRource靖秩,資源的使用頻率比較高须眷,這個時候如果從LruCache取資源,LinkHashmap查找資源的效率不是很高的沟突。所以他會設計一個弱引用來緩存當前活躍資源柒爸,來替Lrucache減壓。

參考:
Android Glide4.0 源碼遨游記(第五集——緩存機制)
Android Glide4.0 源碼遨游記(第四集)
Glide 系列-3:Glide 緩存的實現(xiàn)原理(4.8.0)
Glide 系列-2:主流程源碼分析(4.8.0)
[Glide的圖片內存優(yōu)化]
(http://www.reibang.com/p/12e318e6414f)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末事扭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子乐横,更是在濱河造成了極大的恐慌求橄,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葡公,死亡現(xiàn)場離奇詭異罐农,居然都是意外死亡,警方通過查閱死者的電腦和手機催什,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門涵亏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事气筋〔鹉冢” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵宠默,是天一觀的道長麸恍。 經常有香客問我,道長搀矫,這世上最難降的妖魔是什么抹沪? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮瓤球,結果婚禮上融欧,老公的妹妹穿的比我還像新娘。我一直安慰自己卦羡,他們只是感情好噪馏,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著虹茶,像睡著了一般逝薪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蝴罪,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天董济,我揣著相機與錄音,去河邊找鬼要门。 笑死虏肾,一個胖子當著我的面吹牛,可吹牛的內容都是我干的欢搜。 我是一名探鬼主播封豪,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炒瘟!你這毒婦竟也來了吹埠?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤疮装,失蹤者是張志新(化名)和其女友劉穎缘琅,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體廓推,經...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡刷袍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了樊展。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呻纹。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡堆生,死狀恐怖,靈堂內的尸體忽然破棺而出雷酪,到底是詐尸還是另有隱情淑仆,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布太闺,位于F島的核電站糯景,受9級特大地震影響,放射性物質發(fā)生泄漏省骂。R本人自食惡果不足惜蟀淮,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钞澳。 院中可真熱鬧怠惶,春花似錦、人聲如沸轧粟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兰吟。三九已至通惫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間混蔼,已是汗流浹背履腋。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惭嚣,地道東北人遵湖。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像晚吞,于是被迫代替她去往敵國和親延旧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內容