天天看點

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

一 前言

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 實作的整體結構圖:

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

(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 實作的整體結構圖:

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

(3)W3C規範Cache API的要求

  • Window

     和 

    WorkerGlobalScope

     都提供了 caches 對象, 提供了一系列異步方法,可以建立和操作 Cache 對象。
  • 一個域名( 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的存儲管理對象在浏覽器核心是如何被建立的呢?請看代碼流程:

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的存儲後端。

它們之間的詳細關系,請參考下圖:

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

https://www.atatech.org/articles/81466#3 四 Cache的實作

(1)Cache的建立

我們知道,規範裡 

 對應核心的ServiceWorkerCache對象,提供了已緩存的 

 / 

Response

 對象體的存儲管理機制。它提供了一系列管理存儲的JS接口。

前端開發者可以使用 

 來擷取

 對象的執行個體.

我們看看這種建立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。

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

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)。

它們之間的關系,請參考下圖:

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

(3)ServiceWorker Script Cache相關對象關系

Script Cache相關對象的關系如下:

        4. ServiceWorkerContextCore持有一個ServiceWorkerStorage。

        5. ServiceWorkerStorage管理ServiceWorker Script相關的存儲,其中使用SimpleBackend存儲Script檔案本身,使用LevelDB存儲ServiceWorker注冊資訊。

        6. ServiceWorkerStorage持有一個disk cache的SimpleBackend作為存儲後端。

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

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去查找,還找不到就會走網絡。

PWA系列 - Cache API 的設計與實作一 前言 二 設計思想 三 CacheStorage的實作 四 Cache的實作 五 Script Cache的實作 六 各種Cache的關系 七 綜述 八 參考文檔

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的優勢。

https://www.atatech.org/articles/81466#7 八 參考文檔

Implement Cache and CacheStorage for ServiceWorkers - Mozilla Implement Cache API for ServiceWorker - Chromium Design of Cache API in Blink Implementing the Service Worker Cache API in Gecko

繼續閱讀