本文首發於 vivo互聯網技術 微信公眾號
鏈接:https://mp.weixin.qq.com/s/cPLkefpEb3w12-uoiqzTig
作者:連凌能
Android上圖片加載的解決方案有多種,但是官方認可的是Glide。Glide提供簡潔易用的api,整個框架也方便擴展,比如可以替換網絡請求庫,同時也提供了完備的緩存機制,應用層不需要自己去管理圖片的緩存與獲取,框架會分成內存緩存,文件緩存和遠程緩存。本文不會從簡單的使用着手,會把重點放在緩存機制的分析上。
一、綜述
開始之前,關於Glide緩存請先思考幾個問題:
-
Glide有幾級緩存?
-
Glide內存緩存之間是什么關系?
-
Glide本地文件IO和網絡請求是一個線程嗎?如果不是,怎么實現線程切換?
-
Glide網絡請求回來后數據直接返回給用戶還是先存再返回?
加載開始入口從Engine.load()開始,先看下對這個方法的注釋,
-
會先檢查(Active Resources),如果有就直接返回,Active Resources沒有被引用的資源會放入Memory Cache,如果Active Resources沒有,會往下走。
-
檢查Memory Cache中是否有需要的資源,如果有就返回,Memory Cache中沒有就繼續往下走。
-
檢查當前在運行中的job中是否有改資源的下載,有就在現有的job中直接添加callback返回,不重復下載,當然前提是計算得到的key是一致的,如果還是沒有,就會構造一個新的job開始新的工作。
ok, find the source code.
二、內存緩存
先看到 focus 1,這一步會從 ActiveResources 中加載資源,首先判斷是否使用內存緩存,否的話返回null;否則到 ActiveResources 中取數據:
接下來看下ActiveResources, 其實是用過弱引用保存使用過的資源。
成功取到數據后回調類型也是內存緩存:
接着回到Engine.load()中繼續看到focus 2,如果在cache中找到就是remove掉,然后返回EngineResource,其中需要EngineResource進行acquire一下,這個后面再看,然后會把資源移到ActiveResources中,也就是上面提到的緩存:
其中cache是MemoryCache接口的實現,如果沒設置,默認在build的時候是LruResourceCache, 也就是熟悉的LRU Cache:
再看下EngineResource,主要是對資源增加了引用計數的功能:
在release后會判斷引用計數是否為0,如果是0就會回調onResourceReleased,在這里就是Engine,然后會把資源從ActiveResources中移除,資源默認是可緩存的,因此會把資源放到LruCache中。
如果是回收呢,看看上面的EngineResource,如果引用計數為0並且還沒與回收,就會調用真正的Resource.recycle(),看其中的一個BitmapResource是怎么回收的,就是放到Bitmap池中,也是用的LRU Cache,這個和今天的主題不相關,就不繼續往下拓展。
思路再拉到Engine.load()的流程中,接下來該看focus 3,這里再貼一下代碼,如果job已經在運行了,那么直接添加一個回調后返回LoadStatus,這個可以允許用戶取消任務:
接着往下看到focus 4, 到這里就需要創建后台任務去拉取磁盤文件或者發起網絡請求。
三、磁盤緩存
先構造兩個job,一個是EngineJob,另外一個DecodeJob,其中DecodeJob會根據需要解碼的資源來源分成下面幾個階段:
在構造DecodeJob時會把狀態置為INITIALIZE。
構造完兩個 Job 后會調用 EngineJob.start(DecodeJob),首先會調用getNextStage來確定下一個階段,這里面跟DiskCacheStrategy這個傳入的磁盤緩存策略有關。
磁盤策略有下面幾種:
-
**ALL: **緩存原始數據和轉換后的數據
-
**NONE: **不緩存
-
**DATA: **原始數據,未經過解碼或者轉換
-
**RESOURCE: **緩存經過解碼的數據
-
**AUTOMATIC(默認):**根據`EncodeStrategy`和`DataSource`等條件自動選擇合適的緩存方
默認的AUTOMATIC方式是允許解碼緩存的RESOURCE:
所以在 getNextStage 會先返回Stage.RESOURCE_CACHE,然后在start中會返回diskCacheExecutor,然后開始執行DecodeJob:
DecodeJob會回調run()開始執行, run()中調用runWrapped執行工作,這里runReason還是RunReason.INITIALIZE ,根據前面的分析指導這里會獲得一個ResourceCacheGenerator,然后調用runGenerators:
在 runGenerators 中,會調用 startNext,目前currentGenerator是ResourceCacheGenerator, 那么就是調用它的startNext方法:
看下ResourceCacheGenerator.startNext(), 這里面就是重點邏輯了,首先從Registry中獲取支持資源類型的ModelLoader(其中ModelLoader是在構造Glide的時候傳進去), 然后從ModelLoader中構造LoadData,接着就能拿到DataFetcher,(關於ModelLoader/LoadData/DataFetcher之間的關系不在本次范圍內,后面有機會再另寫)通過它的loadData方法加載數據:
如果在Resource中找不到需要的資源,那么startNext就會返回false,在runGenerators中就會進入循環體內:
-
接着會重復上面執行getNextStage,由於現在Stage已經是RESOURCE_CACHE,所以接下來會返回DataCacheGenerator,執行邏輯和上面的ResourceCacheGenerator是一樣的,如果還是沒有找到需要的,進入循環體內。
-
此時getNextStage會根據用於是否設置只從磁盤中獲取資源,如果是就會通知失敗,回調onLoadFailed;如果不是就設置當前Stage為Stage.SOURCE,接着往下走。
-
狀態就會進入循環內部的if條件邏輯里面,調用reschedule。
-
在reschedule把runReason設置成SWITCH_TO_SOURCE_SERVICE,然后通過callback回調。
-
DecodeJob中的callback是EngineJob傳遞過來的,所以現在返回到EngineJob。
-
在EngineJob中通過getActiveSourceExecutor切換到網絡線程池中,執行DecodeJob,下面就准備開始發起網絡請求。
四、網絡緩存
在Stage.SOURCE階段,通過getNextGenerator返回的是SourceGenerator,所以目前的currentGenerator就是它。
流程還是一樣的,SourceGenerator還是調用startNext方法,獲取到對應的DataFetcher,這里其實是HttpUrlFetcher,發起網絡請求。
先緩一緩,本文其實到了上面已經可以結束了,Glide涉及到的五級緩存都已經涉及到了,是真的就可以結束了嗎?不是的,網絡請求回來和緩存還有關系嗎?接着看到HttpUrlFetcher,下載成功后回調onDataReady,其中callback是SourceGenerator:
正常情況會進入if判斷邏輯里面,賦值dataToCache,然后回調cb.reschedule,而cb就是DecodeJob構造SourceGenerator的時候傳入,cb是DecodeJob。
DecodeJob在reschedule回調EngineJob,最后還是回到SourceGenerator中的startNext()邏輯。
和第一次進來的邏輯不一樣,現在dataToCache != null,進入第一個if邏輯。
在邏輯里面調用cacheData,邏輯很明顯,保持數據到本地,然后會構造一個DataCacheGenerator。
而DataCacheGenerator前面已經分析過了,就是用來加載本地原始數據的,這回會加載成功,返回true。
接下來就是一系列的回調了:
DataCacheGenerator的startNext邏輯里面會給DataFetcher傳遞自身作為callback,在加載本地數據成功后回調onDataReady。
而cb現在是SourceGenerator傳遞過來,SourceGenerator再回調它自己的cb,是DecodeJob在構造它的時候傳過來。
在上面SourceGenerator把DecodeJob切換到ActiveSourceExecutor線程中執行,還記得一開始DecodeJob是在哪啟動的嗎?在EngineJob中啟動,然后是把DecodeJob放到diskCacheExecutor中執行。
所以上面在DecodeJob的onDataFetcherReady會走到第一個if邏輯里面,然后賦值runReason = RunReason.DECODE_DATA,再一次回調Engine.reschedule,將工作線程切換到ActiveSourceExecutor。
然后還是走到DecodeJob, 現在會進入DECODE_DATA分支,在這里面會調用ResourceDecoder把數據解碼:
解碼成功后調用notifyComplete(result, dataSource);
五、總結
現在回答一下開頭的幾個問題。
1、有幾級緩存?五級,分別是什么?
-
活動資源 (Active Resources)
-
內存緩存 (Memory Cache)
-
資源類型(Resource Disk Cache)
-
原始數據 (Data Disk Cache)
-
網絡緩存
2、Glide內存緩存之間是什么關系?
專門畫了一幅圖表明這個關系,言簡意賅。

3、Glide本地文件IO和網絡請求是一個線程嗎?
明顯不是,本地IO通過diskCacheExecutor,而網絡IO通過ActiveSourceExecutor
4、Glide網絡請求回來后數據直接返回給用戶還是先存再返回?
不是直接返回給用戶,會在SourceGenerator中構造一個DataCacheGenerator來取數據。
更多內容敬請關注 vivo 互聯網技術 微信公眾號

注:轉載文章請先與微信號:labs2020 聯系
