一 前言
Cache API是
ServiceWorker的一種新的
應用緩存機制,它提供了可程式設計的緩存操作方式, 能實作各種緩存政策,可以非常細粒度的操控資源緩存。
但我們對Cache API的了解也僅限于此?Cache API在浏覽器的存儲結構是怎樣的,在存儲容量方面有什麼限制,在技術上是如何實作的,為什麼會這樣去設計?
本文嘗試分析解答Cache API相關問題, 讓大家對Cache API有更加深入的了解。
https://www.atatech.org/articles/81466#1 二 設計思想
(1)Chromium Cache API 設計
Chromium Cache API 設計者給
的定位是“
機制”。他們把Cache API定位為
Application Cache,我們就很容易了解Chromium内部Cache API代碼實作會大量重用Application Cache的代碼,使用一樣的存儲類型(
Temporary),使用一樣的存儲後端(
Very Simple Backend)。
至于為什麼這樣定位,目前還未找到官方的解析,我自己的了解是,Chromium在最初設計Cache API時,僅僅是為了給ServiceWorker提供一個加強版的Application Cache。後來Cache API在成為W3C規範的過程中,各方積極參與讨論需求和實作,它的内涵才越來越豐富,它的使用場景也不再局限于ServiceWorker。
Chromium Cache API 實作的整體結構圖:

(2)Firefox Cache API 設計
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實作一套新的存儲機制。使用SQLite的原因是:
- SQLite支援transaction。
- SQLite是一個經過充分測試的系統,大家都非常清楚它的注意事項和性能特征。
- SQL提供了靈活的查詢引擎來實作和微調Cache比對算法。
Firefox Cache API 實作的整體結構圖:
(3)W3C規範Cache API的要求
-
和Window
都提供了 caches 對象, 提供了一系列異步方法,可以建立和操作 Cache 對象。WorkerGlobalScope
- 一個域名( origin )可以有多個不同名稱的 對象,同一域名下的cacheName + Cache由同一 name to cache map 管理。
- 不能在不同域名之間共享,完全獨立于浏覽器的HTTP cache,但同一域名下的Window對象和ServiceWorker對象可以共用。
- 完全由開發者控制,增加,删除,更新,等等操作,都需要由開發者去控制。
注意:下文隻讨論Chromium Blink核心Cache API的設計實作。
https://www.atatech.org/articles/81466#2 三 CacheStorage的實作
(1)CacheStorage的建立
我們知道,規範裡
CacheStorage對應的核心的ServiceWorkerCacheStorage對象,
管理一系列
對象,它提供了很多JS接口用于操作
對象。
- CacheStorage.open() 用于擷取一個 對象執行個體。
- CacheStorage.match() 用于檢查 中是否存在以 Request 為Key的
- CacheStorage.has() 用于檢查是否存在指定名稱的
- CacheStorage.keys() 用于傳回 中所有 對象的Key清單。
- CacheStorage.delete() 用于删除指定名稱的
那麼,CacheStorage的存儲管理對象在浏覽器核心是如何被建立的呢?請看代碼流程:
self.caches.open(cacheName)
--> blink::CacheStorage::open
--> blink::ServiceWorkerCacheStorageDispatcher::dispatchOpen
--> content::ServiceWorkerCacheListener::OnCacheStorageOpen
--> content::ServiceWorkerCacheStorageManager::OpenCache
--> content::ServiceWorkerCacheStorageManager::FindOrCreateServiceWorkerCacheManager
--> new ServiceWorkerCacheStorage
一些需要注意的點:
- 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管理了同一域名下所有的cacheName+ServiceWorkerCache。
-
同一域名下的ServiceWorkerCacheStorage都放在同一目錄,目錄路徑如下,
storage_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003
其中8f9fa7c394456a3f75c7c0aca39d897179ba4003是origin(
)的hash值(使用base::SHA1HashString計算)。
(2)CacheStorage相關對象關系
CacheStorage有非常多的關聯對象,它們之間的關系如下:
1. 單程序模式的Chromium浏覽器,比如,基于Chrome Android WebView的U4浏覽器,一般會持有一個StoragePartition(對應的存儲區為 /data/data/com.UCMobile/app_core_ucmobile)。
2. 一個StoragePartition會持有一個ServiceWorkerContext。
3. ServiceWorkerContextWrapper實作了ServiceWorkerContext,它會持有一個ServiceWorkerContextCore。
4. ServiceWorkerContextCore持有一個ServiceWorkerCacheStorageManager。
5. ServiceWorkerCacheStorageManager會為每一個域名建立一個ServiceWorkerCacheStorage。
6. ServiceWorkerCacheStorage會為每一個cacheName建立一個ServiceWorkerCache。
7. 每一個ServiceWorkerCache對應一個SimpleBackend的存儲後端。
它們之間的詳細關系,請參考下圖:
https://www.atatech.org/articles/81466#3 四 Cache的實作
(1)Cache的建立
我們知道,規範裡
對應核心的ServiceWorkerCache對象,提供了已緩存的
/
Response對象體的存儲管理機制。它提供了一系列管理存儲的JS接口。
- Cache.put() 用于把 對象體放進指定的 。
- Cache.add() 用于擷取一個 的 ,并将 。注:等價于 fetch(request) + Cache.put(request, response)。
- Cache.addAll() 用于擷取一組 ,并将該組
- Cache.keys() 用于擷取 中所有Key清單,一般是Request的清單。
- Cache.match() 用于查找是否存在以
- Cache.matchAll() 用于查找是否存在一組以 對象組。
- Cache.delete() 用于删除以 為Key的Cache Entry。注意,Cache不會過期,隻能顯式 删除 。
前端開發者可以使用
來擷取
對象的執行個體.
我們看看這種建立ServiceWorkerCache對象的過程:
self.caches.open(cacheName) // 比如,cacheName: tm/chaoshi-fresh/4.2.17
--> content::ServiceWorkerCacheStorage::OpenCache // cache_map_查詢不到cacheName,即為首次建立
--> content::ServiceWorkerCacheStorage::SimpleCacheLoader::CreateCache
--> content::ServiceWorkerCacheStorage::SimpleCacheLoader::CreateCachePrepDirInPool
--> content::ServiceWorkerCacheStorage::SimpleCacheLoader::CreateCachePreppedDir
--> content::ServiceWorkerCacheStorage::SimpleCacheLoader::CreateServiceWorkerCache
--> content::ServiceWorkerCache::CreatePersistentCache
--> new ServiceWorkerCache
我們看看ServiceWorkerCache對應的存儲目錄,
- origin:
- cache_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003/7353b21ee437f3877043ae17a5d5ba6395fdbd31
- 其中7353b21ee437f3877043ae17a5d5ba6395fdbd31是cacheName(tm/chaoshi-fresh/4.2.17)的hash值(使用base::SHA1HashString計算)。
還有一種情況也需要重新建立ServiceWorkerCache對象,我們看看這類建立的過程:content::ServiceWorkerCacheListener::OnCacheStorageOpen
--> content::ServiceWorkerCacheStorage::OpenCache // cache_map_可以查詢到cacheName,非首次建立
--> content::ServiceWorkerCacheStorage::GetLoadedCache
// 發現cache_map_中cacheName對應的ServiceWorkerCache對象為空,需要重新建立
這種情況是,ServiceWorkerCache已析構,但ServiceWorkerCacheStorage還未析構,這時在cache_map_可以查詢到cacheName,但裡面的ServiceWorkerCache已為空。
為什麼ServiceWorkerCacheStorage還未析構,而ServiceWorkerCache會已析構呢?
從前面可以看到,ServiceWorkerCacheStorage是由ServiceWorkerCacheStorageManager管理的,而ServiceWorkerCacheStorageManager一般是全局唯一的,即一般ServiceWorkerCacheStorage是不會析構的。
但是,ServiceWorker線程關閉會引起ServiceWorkerCache的析構,流程如下,content::ServiceWorkerDispatcherHost::OnWorkerStopped
--> content::EmbeddedWorkerRegistry::OnWorkerStopped
--> content::EmbeddedWorkerInstance::OnStopped
--> content::ServiceWorkerVersion::OnStopped
--> content::ServiceWorkerCacheListener::~ServiceWorkerCacheListener
--> content::ServiceWorkerCache::~ServiceWorkerCache
是以,就會出現上面描述的ServiceWorkerCacheStorage還未析構,而ServiceWorkerCache會已析構的情況。
(2)Cache的存儲限制
規範并沒有明确規定ServiceWorkerCache的容量限制,那麼,Chromium核心的浏覽器是如何限制的呢?
每個ServiceWorkerCache對象的容量, Chromium40核心限制為512M,
Chromium50及以上版本核心不作限制(即為std::numeric_limits<int>::max)。
當然,這隻是ServiceWorker層面的限制,它還會受浏覽器QuotaManager的限制。
QuotaManager對每個域名可用存儲空間也有限制,算法(Chromium57)可簡單描述如下,
類型存儲限額 = 【系統磁盤可用空間(available_disk_space) + 浏覽器全局已使用空間(global_limited_usage)】/ 3 (注:kTemporaryQuotaRatioToAvail = 3)
每個域名可使用
類型存儲限額 =
類型存儲限額 / 5 (注:QuotaManager::kPerHostTemporaryPortion = 5)
比如,系統磁盤可用空間為570M, 浏覽器全局已使用空間為30M,那麼 每個域名可使用
類型存儲限額 = (570+30)/ 3 / 5 = 40M。
上述例子中,雖然ServiceWorkerCache在ServiceWorker層面的限制為512M,非常大,但它也不能超出每個域名的限制(40M),即同一域名下的ServiceWorkerCache也隻能使用40M。
一般來說,ServiceWorker層面對ServiceWorkerCache的限制都會大于浏覽器對每個域名的限制,是以,通常可了解為,ServiceWorkerCache僅受浏覽器QuotaManager對域名可使用存儲的限制。
(3)Cache的存儲後端
前面提到,每個ServiceWorkerCache會對應一個SimpleBackend的存儲後端。
那麼,這個SimpleBackend是如何建立的呢?請看代碼流程:
content::ServiceWorkerCache::Match
--> content::ServiceWorkerCache::Init // 檢查是否已初始化,如果還未初始化,就會進行初始化
--> content::ServiceWorkerCache::CreateBackend // 初始化的過程會建立SimpleBackend
--> disk_cache::CreateCacheBackend
--> new CacheCreator
--> CacheCreator::Run
--> new disk_cache::SimpleBackendImpl
從上述流程可以看到,ServiceWorkerCache相關方法(比如,match)的調用,會檢查它是否已初始化,如果還未初始化,就會進行初始化,初始化的過程會建立SimpleBackend。
(4)Cache Entry的建立
ServiceWorkerCache提供了已緩存的
對象體的存儲管理機制。這些
對象體就作為ServiceWorkerCache對應的SimpleBackend的Entrys。
我們看看這些Entry是如何建立的,請看代碼流程:
content::ServiceWorkerCache::Put
--> content::ServiceWorkerCache::PutImpl
--> disk_cache::SimpleBackendImpl::CreateEntry // 建立Entry
--> disk_cache::SimpleEntryImpl::CreateEntry
--> disk_cache::SimpleSynchronousEntry::CreateEntry
--> disk_cache::SimpleSynchronousEntry::InitializeForCreate
--> disk_cache::SimpleSynchronousEntry::CreateFiles // 建立Entry對應的檔案
ServiceWorkerCache的put或add等方法,會引起它對應的SimpleBackend Entry的建立,每個Entry會對應一個檔案。
我們看看Entry檔案的存儲目錄,
Entry File Name: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003/7353b21ee437f3877043ae17a5d5ba6395fdbd31/3d1d89ddbe7c000f_0
其中,3d1d89ddbe7c000f_0 是由檔案名(比如,
https://g.alicdn.com/??tm/chaoshi-fresh/4.2.17/index.bundle.css)和檔案索引(比如,file_index: 0)一起生成的一個hash值。
(5)Cache相關對象關系
Cache有非常多的關聯對象,它們之間的關系如下:
1. 規範裡
對應核心的ServiceWorkerCache對象。
2. ServiceWorkerCacheStorage會為每一個cacheName建立一個ServiceWorkerCache。
3. 每個ServiceWorkerCache有一個SimpleBackend。
4. 每個SimpleBackend有若幹個SimpleEntry。
5. 每個SimpleEntry有一個檔案。 比如天貓頁面的cacheName(tm/chaoshi-fresh/4.2.17)對應有多個SimpleEntry檔案,其中一個SimpleEntry檔案為https://g.alicdn.com/tm/chaoshi-fresh/4.2.17/index.bundle.css。
https://www.atatech.org/articles/81466#4 五 Script Cache的實作
(1)ServiceWorker Script Cache的建立
上面介紹了ServiceWorkerCache和ServiceWorkerCacheStorage的實作,它們負責管理ServiceWorker控制的資源的緩存。
那麼,ServiceWorker Script(比如,serviceworker.js)本身是如何存儲的呢?
我們先來看看ServiceWorker Script建立Cache Backend的過程:
content::ServiceWorkerWriteToCacheJob::OnResponseStarted
--> content::ServiceWorkerWriteToCacheJob::WriteHeadersToCache
--> content::ServiceWorkerStorage::CreateResponseWriter
--> content::ServiceWorkerStorage::disk_cache // 如果disk_cache_為空,才繼續建立
--> content::AppCacheDiskCache::InitWithDiskBackend
--> content::AppCacheDiskCache::Init
其中,建立Cache Backend的參數如下,
- cache_type:3 (APP_CACHE)
- backend_type:2 (CACHE_BACKEND_SIMPLE )
- max_bytes:262144000 (250M)
- cache_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/Cache
我們可以看到,所有ServiceWorker Script共用同一存儲後端(SimpleBackend),共用同一存儲目錄,存儲大小限制為250M,存儲類型為APP_CACHE, Backend類型為CACHE_BACKEND_SIMPLE。
(2)ServiceWorker Script Entry的建立
從上面可以看到ServiceWorker Script會使用SimpleBackend作為存儲後端,那麼,它的Entry是怎麼建立的呢?
我們先看看代碼的流程,
--> content::AppCacheResponseWriter::CreateEntryIfNeededAndContinue
--> content::AppCacheDiskCache::CreateEntry
--> disk_cache::SimpleSynchronousEntry::CreateFiles // 建立對應的檔案
建立Entry的詳細資訊如下,
- url: https://ucbrowser.github.io/pwa/message-channel2/service-worker-2.js
- key:10
- file_index:0 (注:file_index為0是指需要新建立檔案)
- entry_hash: 8885558157644453297
- entry_path_: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/Cache
- entry_filename: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/Cache/7b4fd8111178d5b1_0
其中,每一個Script URL會對應一個key和entry_hash,entry_hash會有一個file_index,entry_hash經過一定的算法換算,會生成最終的Entry檔案名(比如, 7b4fd8111178d5b1_0)。
它們之間的關系,請參考下圖:
(3)ServiceWorker Script Cache相關對象關系
Script Cache相關對象的關系如下:
4. ServiceWorkerContextCore持有一個ServiceWorkerStorage。
5. ServiceWorkerStorage管理ServiceWorker Script相關的存儲,其中使用SimpleBackend存儲Script檔案本身,使用LevelDB存儲ServiceWorker注冊資訊。
6. ServiceWorkerStorage持有一個disk cache的SimpleBackend作為存儲後端。
https://www.atatech.org/articles/81466#5 六 各種Cache的關系
前面描述了ServiceWorkerCache和ServiceWorker Script Cache,它們和
HTTP Cache是什麼關系呢?
一般來說,浏覽器Browser程序有一個存儲區(StoragePartition),存儲區裡面的各種緩存的關系如下,
1. 每一個StoragePartition會對應一個ServiceWorkerContextCore和一個HTTPCache的執行個體。
2. 每一個ServiceWorkerContextCore會有一個ServiceWorkerStorage和多個ServiceWorkerCacheStorage(注:一般一個域名有一個)。
3. 每個ServiceWorkerStorage會有一個ServiceWorkerDatabase和一個ServiceWorker Script Cache。其中,ServiceWorkerDatabase存儲所有ServiceWorker的注冊資訊,ServiceWorker Script Cache存儲所有ServiceWorker Script檔案。
4. 每個ServiceWorkerCacheStorage可以有多個ServiceWorkerCache。
5. 每個ServiceWorkerCache會有一個SimpleBackend。ServiceWorker Script Cache有一個SimpleBackend。HTTPCache有一個SimpleBackend。
6. ServiceWorker Script Cache和ServiceWorkerCache,在自己的SimpleBackend找不到相應的緩存檔案,就會到HTTPCache的SimpleBackend去查找,還找不到就會走網絡。
https://www.atatech.org/articles/81466#6 七 綜述
上面詳細介紹了ServiceWorker CacheStorage和Script Cache存儲相關的設計和實作。存儲作為浏覽器最基礎的子產品,是非常複雜的,文章隻涉及了裡面比較基礎的内容,更深入的内容需要大家繼續學習研究。
了解ServiceWorker相關的存儲細節,有什麼作用呢,特别是對前端開發者來說,有必要了解這麼細節的内容嗎?
我們先來看看一些問題,
問題一:ServiceWorker線程啟動後為什麼可以立刻進入active狀态呢?
回答:ServiceWorker Script相關的狀态資訊是持久化到leveldb的資料庫的。線程啟動後可以立刻從資料庫中讀取Script的狀态(比如,actvie)。
問題二:為什麼多程序操作ServiceWorker的緩存會出現問題?
回答:ServiceWorker相關緩存的底層存儲都使用了系統的檔案系統(File System),而檔案系統一般是不支援多程序通路的。
問題三:
Cache
是否可以在不同域名下共享?
回答:從上面的分析來看,每個域名(origin)隻會有一個ServiceWorkerCacheStorage(對應規範的
CacheStorage
),每個ServiceWorkerCacheStorage可以有多個ServiceWorkerCache(對應規範的
Cache
(1)同一域名下的ServiceWorkerCacheStorage都放在同一目錄,存儲路徑如下,
其中8f9fa7c394456a3f75c7c0aca39d897179ba4003是origin(
(2)每一個cacheName對應一個ServiceWorkerCache,存儲路徑如下,
https://chaoshi.m.tmall.com/cache_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003/7353b21ee437f3877043ae17a5d5ba6395fdbd31
其中7353b21ee437f3877043ae17a5d5ba6395fdbd31是cacheName(tm/chaoshi-fresh/4.2.17)的hash值(使用base::SHA1HashString計算)
不同域名下,
的目錄是不一樣的(比如,8f9fa7c394456a3f75c7c0aca39d897179ba4003),它下面
的目錄就更加不一樣了(比如,7353b21ee437f3877043ae17a5d5ba6395fdbd31)。
由于不同域名下
的目錄路徑是不一樣的,是以是不能共用的。
問題四:同一域名下不同子路徑下ServiceWorker使用了同樣的cacheName,它們的
Cache
會存儲在同一目錄嗎?
CacheStorage
Cache
每一個cacheName對應一個ServiceWorkerCache,而且cacheName決定了ServiceWorkerCache的存儲目錄,即同一域名同一cacheName會使用同樣的存儲目錄。
是以,同一域名同一cacheName的
Cache
會存儲在同一目錄,這些
Cache
可以被同一域名下的ServiceWorker共用。
注意:前端需要自行管理cacheName,避免不同的ServiceWorker對同一cacheName操作而産生沖突。
上述列舉的一些問題,在未了解ServiceWorker Cache存儲相關的知識之前,我們很難較好的回答。了解ServiceWorker相關的存儲細節,有助于加深了解ServiceWorker的一些功能和特性。
希望大家能深入了解ServiceWorker的存儲體系,進而能更好的使用Cache API, 更好的發揮Cache API的優勢。