OkHttp3原始碼分析[DiskLruCache]

yangxi_001發表於2017-06-19

OkHttp系列文章如下

本文目錄:

  • Cache的簡介
  • LinkedHashMap原理
  • OkHttp的檔案系統

本文主要是對put/get過程進行分析,注意快取的判斷依據不是本文,而是快取策略


1. Cache的簡介

快取,顧名思義,也就是方便使用者快速的獲取值的一種儲存方式。小到與CPU同頻的昂貴的快取顆粒,記憶體,硬碟,網路,CDN反代快取,DNS遞迴查詢,OS頁面置換,Redis資料庫,都可以看作快取。它有如下的特點:

  1. 快取載體與持久載體總是相對的,容量遠遠小於持久容量,成本高於持久容量,速度高於持久容量。比如硬碟與網路,目前主流的SSD硬碟可以達到500MB/S,而很多地區網速卻只有4M,將網路中的檔案存到硬碟中,硬碟就相當於快取;再比如記憶體與硬碟,主流的DDR3記憶體的速度可以達到10GB/S,而硬碟相對的慢了很多數量級別,將硬碟的遊戲載入到記憶體,記憶體就相對於硬碟是一種快取。
  2. 需要實現排序依據,在java中,可以使用Comparable<T>作為排序的的介面
  3. 需要一種頁面置換演算法(page replacement algorithm)將舊頁面去掉換成新的頁面,如最久未使用演算法(LFU)、先進先出演算法(FIFO)、最近最少使用演算法(LFU)、非最近使用演算法(NMRU)等
  4. 如果沒有命中快取,就需要從原始地址獲取,這個步驟叫做“回源”,CDN廠商會標註“回源率”作為賣點

在OkHttp中,使用FileSystem作為快取載體(磁碟相對於網路的快取),使用LRU作為頁面置換演算法(封裝了LinkedHashMap)。

  1. Comparable<T>是java用來排序的介面,推薦參考閱讀《Java Software Structures Designing and Using Data Structures》
  2. 頁面置換演算法可以參考閱讀《現代作業系統》的中譯本

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中,使用了DiskLruCacheLinkedHashMap進行了封裝實現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>

上述具體程式碼沒有原始碼分析哦,王垠大神看了都會頭大

  1. 需要複習HashMap原始碼?可以考慮閱讀HashMap原理文章
  2. 本部分基於JDK1.8.0_05,可能部分函式與網上文章相沖突
  3. 在golang中,使用ringmap實現了Lru,可以看這裡

3. OkHttp的檔案系統

OkHttp中的關鍵物件如下:

  • FileSystem: 使用Okio對File的封裝,簡化了IO操作
  • DiskLruCache.Editor: 新增了同步鎖,並對FileSystem進行高度封裝
  • DiskLruCache.Entry: 維護著key對應的多個檔案
  • Cache.Entry: Responsejava物件與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}},後面的01,分別表示ENTRY_METADATAENTRY_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的檔案中,是存二進位制的過程;

    OkHttp Sink to file
  • 讀取場景:讀取時,需要獲取快照,通過呼叫鏈分析如下

    okhttp snapshot source

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

總結

  1. OkHttp通過對檔案進行了多次封裝,實現了非常簡單的I/O操作
  2. OkHttp通過對請求url進行md5實現了與檔案的對映,實現寫入,刪除等操作
  3. OkHttp內部維護著清理執行緒池,實現對快取檔案的自動清理

Refference

  1. https://zh.wikipedia.org/wiki/快取
  2. https://en.wikipedia.org/wiki/Page_replacement_algorithm#Least_recently_used
  3. http://stackoverflow.com/questions/2889777/difference-between-hashmap-linkedhashmap-and-treemap

相關文章