本文共 13448 字,大约阅读时间需要 44 分钟。
是 的一种新的机制,它提供了可编程的缓存操作方式, 能实现各种缓存策略,可以非常细粒度的操控资源缓存。
但我们对Cache API的了解也仅限于此?Cache API在浏览器的存储结构是怎样的,在存储容量方面有什么限制,在技术上是如何实现的,为什么会这样去设计? 本文尝试分析解答Cache API相关问题, 让大家对Cache API有更加深入的理解。(1)Chromium Cache API 设计
给 的定位是“ 的一种新的机制”。他们把Cache API定位为,我们就很容易理解Chromium内部Cache API代码实现会大量重用Application Cache的代码,使用一样的存储类型(),使用一样的存储后端()。 至于为什么这样定位,目前还未找到官方的解析,我自己的理解是,Chromium在最初设计Cache API时,仅仅是为了给ServiceWorker提供一个加强版的Application Cache。后来Cache API在成为W3C规范的过程中,各方积极参与讨论需求和实现,它的内涵才越来越丰富,它的使用场景也不再局限于ServiceWorker。Chromium Cache API 实现的整体结构图:
(2)Firefox Cache API 设计 在博客文章中描述了他的想法,最初是想重用HTTP Cache 或者 基于IndexedDB去实现,但Cache API规范在不断演进,一些规范细节与上述解决方案存在不可调和的冲突。基于上述原因,Firefox决定基于SQLite为Cache API实现一套新的存储机制。使用SQLite的原因是:
Firefox Cache API 实现的整体结构图:
(3)W3C规范Cache API的要求
和
都提供了对象,提供了一系列异步方法,可以创建和操作对象。注意:下文只讨论Chromium Blink内核Cache API的设计实现。
(1)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 一些需要注意的点:(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的存储后端。 它们之间的详细关系,请参考下图:(1)Cache的创建
我们知道,规范里 对应内核的ServiceWorkerCache对象,提供了已缓存的 / 对象体的存储管理机制。它提供了一系列管理存储的JS接口。前端开发者可以使用 来获取 对象的实例. 我们看看这种创建ServiceWorkerCache对象的过程:self.caches.open(cacheName) // 比如,cacheName: tm/chaoshi-fresh/4.2.17
--> content::ServiceWorkerCacheListener::OnCacheStorageOpen --> 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对应的存储目录,还有一种情况也需要重新创建ServiceWorkerCache对象,我们看看这类创建的过程:content::ServiceWorkerCacheListener::OnCacheStorageOpen
--> content::ServiceWorkerCacheStorage::OpenCache // cache_map_可以查询到cacheName,非首次创建 --> content::ServiceWorkerCacheStorage::GetLoadedCache // 发现cache_map_中cacheName对应的ServiceWorkerCache对象为空,需要重新创建 --> content::ServiceWorkerCacheStorage::SimpleCacheLoader::CreateServiceWorkerCache --> content::ServiceWorkerCache::CreatePersistentCache --> new 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,不作限制(即为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 是由文件名(比如,)和文件索引(比如,file_index: 0)一起生成的一个hash值。 (5)Cache相关对象关系 Cache有非常多的关联对象,它们之间的关系如下: 1. 规范里对应的内核的ServiceWorkerCacheStorage对象, 对应内核的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。 它们之间的详细关系,请参考下图:(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 --> disk_cache::CreateCacheBackend --> new CacheCreator --> CacheCreator::Run --> new disk_cache::SimpleBackendImpl 其中,创建Cache Backend的参数如下,我们可以看到,所有ServiceWorker Script共用同一存储后端(SimpleBackend),共用同一存储目录,存储大小限制为250M,存储类型为APP_CACHE, Backend类型为CACHE_BACKEND_SIMPLE。
(2)ServiceWorker Script Entry的创建 从上面可以看到ServiceWorker Script会使用SimpleBackend作为存储后端,那么,它的Entry是怎么创建的呢? 我们先看看代码的流程, content::ServiceWorkerWriteToCacheJob::OnResponseStarted --> content::ServiceWorkerWriteToCacheJob::WriteHeadersToCache --> content::AppCacheResponseWriter::CreateEntryIfNeededAndContinue --> content::AppCacheDiskCache::CreateEntry --> disk_cache::SimpleBackendImpl::CreateEntry // 创建Entry --> disk_cache::SimpleEntryImpl::CreateEntry --> disk_cache::SimpleSynchronousEntry::CreateEntry --> disk_cache::SimpleSynchronousEntry::InitializeForCreate --> disk_cache::SimpleSynchronousEntry::CreateFiles // 创建对应的文件 创建Entry的详细信息如下,其中,每一个Script URL会对应一个key和entry_hash,entry_hash会有一个file_index,entry_hash经过一定的算法换算,会生成最终的Entry文件名(比如, 7b4fd8111178d5b1_0)。
它们之间的关系,请参考下图: (3)ServiceWorker Script Cache相关对象关系 Script Cache相关对象的关系如下: 1. 单进程模式的Chromium浏览器,比如,基于Chrome Android WebView的U4浏览器,一般会持有一个StoragePartition(对应的存储区为 /data/data/com.UCMobile/app_core_ucmobile)。 2. 一个StoragePartition会持有一个ServiceWorkerContext。 3. ServiceWorkerContextWrapper实现了ServiceWorkerContext,它会持有一个ServiceWorkerContextCore。 4. ServiceWorkerContextCore持有一个ServiceWorkerStorage。 5. ServiceWorkerStorage管理ServiceWorker Script相关的存储,其中使用SimpleBackend存储Script文件本身,使用LevelDB存储ServiceWorker注册信息。 6. ServiceWorkerStorage持有一个disk cache的SimpleBackend作为存储后端。 它们之间的关系,请参考下图:前面描述了ServiceWorkerCache和ServiceWorker Script 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去查找,还找不到就会走网络。 它们之间的关系,请参考下图:上面详细介绍了ServiceWorker CacheStorage和Script Cache存储相关的设计和实现。存储作为浏览器最基础的模块,是非常复杂的,文章只涉及了里面比较基础的内容,更深入的内容需要大家继续学习研究。
理解ServiceWorker相关的存储细节,有什么作用呢,特别是对前端开发者来说,有必要了解这么细节的内容吗?
我们先来看看一些问题,
问题一:ServiceWorker线程启动后为什么可以立刻进入active状态呢?回答:ServiceWorker Script相关的状态信息是持久化到leveldb的数据库的。线程启动后可以立刻从数据库中读取Script的状态(比如,actvie)。问题二:为什么多进程操作ServiceWorker的缓存会出现问题?回答:ServiceWorker相关缓存的底层存储都使用了系统的文件系统(File System),而文件系统一般是不支持多进程访问的。
问题三: 是否可以在不同域名下共享?回答:从上面的分析来看,每个域名(origin)只会有一个ServiceWorkerCacheStorage(对应规范的 ),每个ServiceWorkerCacheStorage可以有多个ServiceWorkerCache(对应规范的 )。(1)同一域名下的ServiceWorkerCacheStorage都放在同一目录,存储路径如下,
storage_path: /data/data/com.UCMobile/app_core_ucmobile/Service Worker/CacheStorage/8f9fa7c394456a3f75c7c0aca39d897179ba4003 其中8f9fa7c394456a3f75c7c0aca39d897179ba4003是origin()的hash值(使用base::SHA1HashString计算)。 (2)每一个cacheName对应一个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计算) 不同域名下,的目录是不一样的(比如,8f9fa7c394456a3f75c7c0aca39d897179ba4003),它下面的目录就更加不一样了(比如,7353b21ee437f3877043ae17a5d5ba6395fdbd31)。由于不同域名下 的目录路径是不一样的,所以是不能共用的。问题四:同一域名下不同子路径下ServiceWorker使用了同样的cacheName,它们的会存储在同一目录吗?回答:从上面的分析来看,每个域名(origin)只会有一个ServiceWorkerCacheStorage(对应规范的 ),每个ServiceWorkerCacheStorage可以有多个ServiceWorkerCache(对应规范的 )。每一个cacheName对应一个ServiceWorkerCache,而且cacheName决定了ServiceWorkerCache的存储目录,即同一域名同一cacheName会使用同样的存储目录。所以,同一域名同一cacheName的会存储在同一目录,这些可以被同一域名下的ServiceWorker共用。注意:前端需要自行管理cacheName,避免不同的ServiceWorker对同一cacheName操作而产生冲突。
上述列举的一些问题,在未了解ServiceWorker Cache存储相关的知识之前,我们很难较好的回答。理解ServiceWorker相关的存储细节,有助于加深理解ServiceWorker的一些功能和特性。
希望大家能深入理解ServiceWorker的存储体系,从而能更好的使用Cache API, 更好的发挥Cache API的优势。
转载地址:http://rpcwo.baihongyu.com/