OkHttp3原始碼分析[DiskLruCache]
OkHttp系列文章如下
本文目錄:
- Cache的簡介
- LinkedHashMap原理
- OkHttp的檔案系統
本文主要是對put/get過程進行分析,注意快取的判斷依據不是本文,而是快取策略
1. Cache的簡介
快取,顧名思義,也就是方便使用者快速的獲取值的一種儲存方式。小到與CPU同頻的昂貴的快取顆粒,記憶體,硬碟,網路,CDN反代快取,DNS遞迴查詢,OS頁面置換,Redis資料庫,都可以看作快取。它有如下的特點:
- 快取載體與持久載體總是相對的,容量遠遠小於持久容量,成本高於持久容量,速度高於持久容量。比如硬碟與網路,目前主流的SSD硬碟可以達到500MB/S,而很多地區網速卻只有4M,將網路中的檔案存到硬碟中,硬碟就相當於快取;再比如記憶體與硬碟,主流的DDR3記憶體的速度可以達到10GB/S,而硬碟相對的慢了很多數量級別,將硬碟的遊戲載入到記憶體,記憶體就相對於硬碟是一種快取。
- 需要實現
排序依據
,在java中,可以使用Comparable<T>
作為排序的的介面 - 需要一種
頁面置換演算法(page replacement algorithm)
將舊頁面去掉換成新的頁面,如最久未使用演算法(LFU)、先進先出演算法(FIFO)、最近最少使用演算法(LFU)、非最近使用演算法(NMRU)等 - 如果沒有命中快取,就需要從原始地址獲取,這個步驟叫做“回源”,CDN廠商會標註“回源率”作為賣點
在OkHttp中,使用FileSystem
作為快取載體(磁碟相對於網路的快取),使用LRU作為頁面置換演算法(封裝了LinkedHashMap)。
Comparable<T>
是java用來排序的介面,推薦參考閱讀《Java Software Structures Designing and Using Data Structures》- 頁面置換演算法可以參考閱讀《現代作業系統》的中譯本
2. LinkedHashMap原理
2.1. 原始碼概述分析
在學習之前,我們要了解一下LinkedHashMap。LinkedHashMap繼承於HashMap。
在HashMap中,維護了一個Node<K,V>[] table
,當put操作時,將元素按照計算出的Hash填到陣列相應位置table[Hash]
中,最後迭代時,從table[0]開始向後迭代,具體的順序取決於元素的HashCode,所以我們常說HashMap的元素迭代是不可預測的。
而在LinkedHashMap中,除了Node<K,V>[] table
,還維護著Entry<K,V>
head,tail
。當put元素後,呼叫下列回撥函式對連結串列將元素移動到鏈尾以及清理舊的元素
// move node to last
void afterNodeAccess(Node<K,V> e)
// possibly remove eldest
void afterNodeInsertion(boolean evict)
在get元素時,如果設定accessOrder
為true時,通過呼叫如下回撥移動元素到鏈尾,這裡特別強調移動,如果這個元素本身已經在連結串列中,那它將只會移動,而不是新建
// move node to last
void afterNodeAccess(Node<K,V> e)
綜上,當你反覆對元素進行get/put操作時,經常使用的元素會被移動到tail
中,而長期不用的元素會被移動到head
最後迭代(Iterator)時,迭代是從舊元素迭代到新元素,這就是LRU的實現
head <--> .... <--> tail
舊元素 <-----------> 反覆使用的新元素
在OkHttp中,使用了DiskLruCache
對LinkedHashMap
進行了封裝實現LRU,按照下圖的方法進行初始化
//按照訪問順序排序的Map,設定accessOrder為true
map = new LinkedHashMap<>(0, 0.75f, true);
2.2. HashMap的對比
以下是常見的3種map的區別,以下均不計算擴容時的時間複雜度
HashMap | LinkedHashMap | TreeMap | |
---|---|---|---|
Performance get/set | O(1) | O(1) | O(logN) |
Implement | Array | Link + Array | Red-Black Tree |
Iteration | unpredictable | put/accessOrder |
Comparable<Key> |
上述具體程式碼沒有原始碼分析哦,王垠大神看了都會頭大
- 需要複習HashMap原始碼?可以考慮閱讀HashMap原理文章
- 本部分基於JDK1.8.0_05,可能部分函式與網上文章相沖突
- 在golang中,使用
ring
與map
實現了Lru,可以看這裡
3. OkHttp的檔案系統
OkHttp中的關鍵物件如下:
- FileSystem: 使用Okio對
File
的封裝,簡化了IO操作 - DiskLruCache.Editor: 新增了同步鎖,並對FileSystem進行高度封裝
- DiskLruCache.Entry: 維護著key對應的多個檔案
- Cache.Entry:
Response
java物件與Okio流
的序列化/反序列化類 - DiskLruCache: 維護著檔案的建立,清理,讀取。內部有清理執行緒池,LinkedHashMap(也就是LruCache)
- Cache: 被上級程式碼呼叫,提供透明的put/get操作,封裝了快取檢查條件與
DiskLruCache
,開發者只用配置大小即可,不需要手動管理 - Response/Requset: OkHttp的請求與迴應
3.1. 檔案初級封裝(FileSystem)
眾所周之,檔案讀寫是流操作,是一大堆的令人頭痛的try/cache操作,在OkHttp中設計了FileSystem.SYSTEM作為檔案層的管理。通過用Okio庫中的Source/Sink
對File進行包裝,而不用更為頭痛的InputStream這類東西,使上層呼叫與管道操作一樣簡單。
File(低階操作,步驟繁瑣) -> Okio(封裝) -> FileSystem(友好工具類)
至於Okio為何這個好,直接去官網參考
3.2. 檔案高階封裝(DiskLruCache.Entry/Editor/Snapshot)
本部分進行了如下的轉換,進行了實際的put/get操作
FileSystem <-- DiskLruCache.Entry/Editor --> source/sink(更少引數)
DiskLruCache.Entry針對每個請求的url對應的檔案進行引用維護(而沒有進行建立/讀取等操作),它內部維護了2個File
陣列,一般來說每個url對應2~4個檔案。
檔名命名規則是{md5(url)+ {0,1}},後面的0
或1
,分別表示ENTRY_METADATA與ENTRY_BODY。
比如在快取的路徑下執行ls
,結果如下
$ ls
5716ab0f06c49bc7cf602397c51d5677.0
5716ab0f06c49bc7cf602397c51d5677.1
5b2f52377611dc6201a1871bdb997466.0
5b2f52377611dc6201a1871bdb997466.1
journal
.....
DiskLruCache.Editor對工具類FileSystem
進行進一步的封裝,它以DiskLruCache.Entry
作為構造引數,通過操控Entry
中維護的陣列,對外暴露source/sink,為上層的java物件與檔案的轉換提供基於okio的流操作,我們可以通過對它的兩個方法進行FindUsage查詢獲得OkHttp關於檔案讀寫的全部場景
- 寫入場景:第一個位置是寫入元資訊,也就是寫入末位是0的檔案中,是序列化的過程;第二個位置是寫入body,也就是寫入末位是1的檔案中,是存二進位制的過程;
- 讀取場景:讀取時,需要獲取快照,通過呼叫鏈分析如下
3.3. 序列化與反序列化(Cache.Entry)
檔案儲存本質上也是序列化與反序列化的過程。本部分提供了下圖的轉變
Resonse(java物件) <--- Cache.Entry ---> source/sink(檔案io)
程式碼部分不復雜,與上面的findusage位置相同,可以概括下:
如果資訊本身就是二進位制,就直接寫到檔案中;如果是文字資訊,按照預設的格式寫入即可。
至於序列化後的東西到底是什麼,可以直接在shell下執行
cat
命令或者開啟文字編輯器進行輸出檢視。注意這裡的
Cache.Entry
與上面的DiskLruCache.Entry
是兩個完全不同的物件
3.4 快取的自動清理
在DiskLruCache初始化時,將建立執行緒池,最少零個執行緒,最大一個執行緒,執行緒空閒可以活60s,執行緒名叫做"OkHttp DiskLruCache",當JVM退出時,執行緒自動結束。
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true))
當需要清理時,執行清理任務,它將在每次get/set後呼叫
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
if (!initialized | closed) {
return; // Nothing to do
}
try {
//遍歷LRU快取(從舊到新進行遍歷map),並刪除檔案
//直到小於MaxSize為止
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
};
總結
- OkHttp通過對檔案進行了多次封裝,實現了非常簡單的I/O操作
- OkHttp通過對請求url進行md5實現了與檔案的對映,實現寫入,刪除等操作
- OkHttp內部維護著清理執行緒池,實現對快取檔案的自動清理
Refference
相關文章
- OkHttp3原始碼解析(一)之請求流程HTTP原始碼
- Android OkHttp3原始碼詳解——整體框架AndroidHTTP原始碼框架
- OkHttp3原始碼解析(三)——連線池複用HTTP原始碼
- Retrofit原始碼分析三 原始碼分析原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(51)rpc原始碼分析原始碼RPC
- 以太坊原始碼分析(52)trie原始碼分析原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- k8s client-go原始碼分析 informer原始碼分析(4)-DeltaFIFO原始碼分析K8SclientGo原始碼ORM
- k8s client-go原始碼分析 informer原始碼分析(6)-Indexer原始碼分析K8SclientGo原始碼ORMIndex
- 5.2 spring5原始碼--spring AOP原始碼分析三---切面原始碼分析Spring原始碼
- Java程式設計架構實戰——OKHTTP3原始碼和設計模式(上篇)Java程式設計架構HTTP原始碼設計模式
- Spring原始碼分析——搭建spring原始碼Spring原始碼
- 精盡MyBatis原始碼分析 - MyBatis-Spring 原始碼分析MyBatis原始碼Spring
- 以太坊原始碼分析(35)eth-fetcher原始碼分析原始碼
- 以太坊原始碼分析(20)core-bloombits原始碼分析原始碼OOM
- 以太坊原始碼分析(24)core-state原始碼分析原始碼
- 以太坊原始碼分析(29)core-vm原始碼分析原始碼
- 以太坊原始碼分析(34)eth-downloader原始碼分析原始碼
- k8s client-go原始碼分析 informer原始碼分析(5)-Controller&Processor原始碼分析K8SclientGo原始碼ORMController
- ddos原始碼分析原始碼
- SpringBoot原始碼分析Spring Boot原始碼
- ucontext原始碼分析Context原始碼
- jQuery原始碼分析jQuery原始碼
- Express原始碼分析Express原始碼
- Eureka原始碼分析原始碼
- AbstractQueuedSynchronizer原始碼分析原始碼
- unbound原始碼分析原始碼
- Mybatis原始碼分析MyBatis原始碼
- apparmor 原始碼分析APP原始碼