解鎖快取新姿勢——更靈活的 Cache

UC核心釋出發表於2017-06-27

快取大家族迎來了新的成員——Cache,可能考慮到 Application Cache、LocalStorage 這兩個傢伙的先天缺陷後天發育不良帶來的問題,Google 和 Firefox 對其進行了基因重組,不過兩邊在如何重組的想法上有分歧,所以各自就分別各自造了去。

Chromium Cache API 設計者給 Cache API 的定位是“ServiceWorker 的一種新的應用快取機制”,他們把 Cache API 定位為 Application Cache(雖然離線快取的設計上存在重大缺陷,但是程式碼至少是無辜的),我們就很容易理解為什麼 Chromium 內部的 Cache API 程式碼實現會大量複用 Application Cache 的程式碼,使用一樣的儲存型別(Temporary),使用一樣的儲存後端(Very Simple Backend)。

Firefox Cache API 設計者 在部落格文章中描述了他的想法,最初是想重用 HTTP Cache 或者 基於 IndexedDB 去實現,但 Cache API 規範在不斷演進,一些規範細節與上述解決方案存在不可調和的衝突。比如,HTTP Cache 中,一個 URL 只能對應一個 Response,但 Cache API 規範要求同一 URL(不同的 Header)可以對應多個 Response,另外,HTTP Cache 沒有使用容量管理系統(QuotaManager)而 Cache API 需要使用。IndexedDB 基於結構克隆(structured cloning),還不支援流式資料(streaming data),這樣,Response可能會非常大,從網路回來會非常慢,會明顯增大記憶體使用。基於上述原因,Firefox 決定基於 SQLite 為 Cache API 實現一套新的儲存機制。

你需要知道 CacheStorage

Cache 物件受到 CacheStorage 的管理,在 W3C 規範中,CacheStorage 對應到核心的 ServiceWorkerCacheStorage 物件。它提供了很多JS介面用於操作Cache 物件:

  • CacheStorage.open() 用於獲取一個 Cache 物件例項。
  • CacheStorage.match() 用於檢查 CacheStorage 中是否存在以Request 為 key 的 Cache 物件。
  • CacheStorage.has() 用於檢查是否存在指定名稱的 Cache 物件。
  • CacheStorage.keys() 用於返回 CacheStorage 中所有 Cache 物件的 cacheName 列表。
  • CacheStorage.delete() 用於刪除指定 cacheName 的 Cache 物件。

在使用過程中,需要注意以下這些情況:

  • 任意 CacheStorage 方法的呼叫,都有機會引起建立 ServiceWorkerCacheStorage 物件。
  • ServiceWorkerCacheStorageManager 維護一個 cache_storage_map_(std::map<GURL, ServiceWorkerCacheStorage*>),這個 map 管理了所有的 origin + ServiceWorkerCacheStorage。
  • 任何一個域名(比如,origin: https://chaoshi.m.tmall.com/)只會建立一個 ServiceWorkerCacheStorage 物件。
  • ServiceWorkerCacheStorage 維護一個 cache_map_(std::map<std::string, base::WeakPtr<ServiceWorkerCache> >),這個 map 管理了同一 origin 下所有的 cacheName + ServiceWorkerCache。
  • 同一域名下的 ServiceWorkerCacheStorage 都放在同一目錄,目錄路徑 storage_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003。其中8f9fa7c394456a3f75c7c0aca39d897179ba4003 是 origin(https://chaoshi.m.tmall.com/)的 hash 值。

前端從這些情況可以得到哪些資訊呢?資源的儲存不是按照資源的域名處理的,而是按照 Service Worker 的 origin 來處理,所以 Cache 的資源是無法跨域共享的,意思就是說,不同域的 Service Worker 無法共享使用對方的 Cache,即使是 Foreign Cache 請求的跨域資源,同樣也是存放在這個 origin 之下。因為 ServiceWorkerCache 通過 cacheName 標記快取版本,所以就會存在多個版本的 ServiceWorkerCache 資源。為什麼需要 cacheName 來標記版本呢?

假設當前域名下所有的覆蓋式釋出的靜態資源和介面資料全部儲存在同一個 cacheName 裡面,業務部署更新後,無法識別舊的冗餘資源,單靠前端無法完全清除。這是因為 Service Worker 不知道完整的靜態資源路徑表,只能在客戶端發起請求時去做判斷,那些當前不會用到的資源不代表以後一定不會使用到。假如靜態資源是非覆蓋式釋出,那麼冗餘的資源就更多了。這裡要特別注意的是,Cache 不會過期,只能顯式刪除

如果版本更新後,更換 cacheName,這意味著舊 cacheName 的資源是不會使用到了,業務邏輯可以放心的把舊 cacheName 對應的所有資源全部清除,而無需知道完整的靜態資源路徑表。

那 cacheName 是不是隻是在這種情況下才能發揮作用呢?其實不是的,使用過 webpack 工具的同學知道 vender 配置,vender 主要是把最不經常變動的第三方的庫檔案打包在一起,避免與頻繁更新的資源打包一起,提高客戶端快取使用率,還有就是 common 的配置,把公用的元件打包在一起,減少程式碼冗餘,因此,cacheName 也可以根據這種情況進行設定,最大化利用快取空間,提高快取利用率。

由於 Service Worker 相關快取的底層儲存都使用了系統的檔案系統(File System),而檔案系統一般是不支援多程式訪問的,當統一域名下有兩個不同的 Service Worker 是無法同時對同一資源進行操作的。

你更需要知道 Cache

規範裡 Cache 對應核心的 ServiceWorkerCache 物件,提供了已快取的 Request / Response 物件體的儲存管理機制。它提供了一系列管理儲存的JS介面:

  • Cache.put() 用於把 Request / Response 物件體放進指定的 Cache。
  • Cache.add() 用於獲取一個 Request 的 Response,並將 Request / Response 物件體放進指定的 Cache。注:等價於 fetch(request) + Cache.put(request, response)。
  • Cache.addAll() 用於獲取一組 Request 的 Response,並將該組 Request / Response 物件體放進指定的Cache。
  • Cache.keys() 用於獲取 Cache 中所有 key。
  • Cache.match() 用於查詢是否存在以 Request 為 key 的Cache 物件。
  • Cache.matchAll() 用於查詢是否存在一組以 Request 為 key 的 Cache 物件組。
  • Cache.delete() 用於刪除以 Request 為 key 的 Cache Entry。注意,Cache 不會過期,只能顯式刪除 。

ServiceWorkerCache 對應的儲存目錄是 /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003/7353b21ee437f3877043ae17a5d5ba6395fdbd31,其中 7353b21ee437f3877043ae17a5d5ba6395fdbd31 是 cacheName(tm/chaoshi-fresh/4.2.17)的 hash 值(使用 base::SHA1HashString 計算)。相同的資源名稱,如果 cacheName 不同,是會分開儲存的哦。

說到儲存路徑,那必然會涉及到儲存的容量大小,Service Worker 規範並沒有明確規定 ServiceWorkerCache 的容量限制,在 Chromium 50 以下版本的核心限制為 512M,Chromium 50 及以上版本核心不作限制(即為std::numeric_limits<int>::max)。當然,這只是 Service Worker 層面的限制,它還會受瀏覽器 QuotaManager 的限制。

QuotaManager 對每個域名可用儲存空間也有限制,演算法(Chromium57)可簡單描述如下:

Temporary 型別儲存限額 = 【系統磁碟可用空間(available_disk_space) + 瀏覽器全域性已使用空間(global_limited_usage)】/ 3 (注:kTemporaryQuotaRatioToAvail = 3)

每個域名可使用 Temporary 型別儲存限額 = Temporary 型別儲存限額 / 5 (注:QuotaManager::kPerHostTemporaryPortion = 5)

比如,系統磁碟可用空間為 570M, 瀏覽器全域性已使用空間為 30M,那麼 每個域名可使用 Temporary 型別儲存限額 = (570+30)/ 3 / 5 = 40M。雖然 ServiceWorkerCache 在 Service Worker 層面的限制為 512M,非常大,但它也不能超出每個域名的限制(40M),即同一域名下的 ServiceWorkerCache 也只能使用 40M。

一般來說,Service Worker 層面對 ServiceWorkerCache 的限制都會大於瀏覽器對每個域名的限制,所以,通常可理解為,ServiceWorkerCache 僅受瀏覽器 QuotaManager 對域名可使用儲存的限制。對於前端開發同學來說,必須有清理冗餘快取的業務邏輯,並且提高快取資源的使用率。

當 Service Worker 從 Cache 拿不到資源時,就會去 http cache 查詢,找不到才去請求網路。

目前對 Cache API 的使用比較有限,後面有經驗積累再繼續補充。


相關文章