承接《ServiceWorker上篇:应用与实践》
本文内容
本文先从整体架构阐述各个模块的定位,再从生命周期、请求网络资源两个流程研究service worker在内核的实现原理以及性能数据。(以下内容基于chromium 57版本)
整体架构
这里从模块的粒度剖析SW整体实现架构,关键类的具体职责见大图。
- Webkit模块分为两层:
- JS接口层。以idl方式与对应V8 Object进行绑定,提供Context以及ServiceWorker、Cache等对象供service-worker.js使用。主要负责对接V8接口以及CSP安全检查。
- Web实现层。分为两部分,一是作为代理传递JS接口层到底层content/child模块的调用;二是管理SW需要用到Webkit模块的资源,例如WebEmbeddedWorkerImpl负责创建一个webview加载service-worker.js,创建workerThread请求网络资源。
content/child模块。负责转发IPC消息,Webkit与browser的中间层,运行于render主线程。ServiceWorkerNetworkProvider用于RenderFrameImpl资源请求时添加provider id标识SW类型。
content/render模块。负责转发IPC消息,Webkit与browser的中间层,运行于render worker线程。
browser模块分为两层:
- SW对外接口层,负责在browser端提供SW能力或调用其他模块能力。其中包含管理SW生命周期以及拦截网络请求两部分,控制SW实现层。
- SW实现层,负责SW具体业务,实现W3C标准。包括注册状态、service-worker.js版本管理、本地disk cache存储、SW进程或线程(取决于平台)创建等。与Web实现层通过render间接通信。
由此可见,service worker标准关键的实现逻辑是Web实现层以及SW实现层,分别实现browser端以及WebKit端功能。
生命周期
注册流程
1.ServiceWorkerContainer::registerServiceWorker
{JS入口,检查是否https、sw.js域名安全、是否与host一致、CSP检查}
2.ServiceWorkerDispatcher::RegisterServiceWorker
{发送IPC到host端}
3.ServiceWorkerDispatcherHost::OnRegisterServiceWorker
{browser端安全检查}
4.ServiceWorkerJobCoordinator::Register
{创建register job,push进队列立即执行}
5.ServiceWorkerRegisterJob::Start
{判断是注册还是更新,检查storage是否已有registration。
如果第一次注册,则创建ServiceWorkerRegistration并保存在ServiceWorkerProviderHost中,并且创建Version,调用StartWorker。
结束后设置状态,异步回调js register函数的ResolvePromise,返回registration。
IPC通知ServiceWorkerGlobalScope::dispatchExtendableEvent触发Install事件}
6.ServiceWorkerVersion::StartWorker
{停止更新sw.js的定时器。(注意sw.js的更新策略在此类实现)
判断SW状态,如果是STOPPED则调用EmbeddedWorkerInstance启动service worker。}
7.EmbeddedWorkerInstance::StartTask::Start
{在browser UI线程以script_url创建RenderProcessHost进程。如果有则复用,没有则通过SiteInstance::CreateForURL创建进程。
在IO线程通过render接口层调用WebEmbeddedWorkerImpl::startWorkerContext创建Webkit端service worker。}
9.EmbeddedWorkerDispatcher::StartWorkerContext
{准备启动数据,例如scriptURL, userAgent, v8CacheOptions等。
创建WebView以及WebLocalFrame。用FrameLoader加载scriptURL域名的空页面(shadow page)。}
10.EmbeddedWorkerDispatcher::didFinishDocumentLoad
{创建ServiceWorkerNetworkProvider用于拦截网络请求以及控制host生命周期。
创建WorkerScriptLoader在worker线程异步拉取并加载scriptURL资源。}
11.EmbeddedWorkerDispatcher::onScriptLoaderFinished
{启动service worker线程。准备启动数据例如IndexedDB, ServiceWorkerGlobalScope等client以及各种settings, scriptURL资源内容,创建ServiceWorkerThread并执行scriptURL的内容。释放之前拉取scriptURL的WorkerScriptLoader。}
存在的问题:
1.注册耗时。从代码路径分析可以看到,第一次注册需要从Webkit端IPC到browser端再IPC回到Webkit,其中还需要在UI、IO、worker线程中切换,加载webview。在ARM64位四核1.3G,内存2G的Android设备上,实测register到成功回调的执行时间是1.1s左右(排除网络拉取页面以及脚本时间);而重启blink内核,第二次打开网页register耗时只需要30ms,耗时差异的原因是第一次注册成功后,SW相关信息以及脚本会保存在本地,第二次加载scope网页时会读取本地信息初始化SW,register时在browser端发现已存在ServiceWorkerVersion则直接返回。
解决方法:对于首次注册耗时的问题,google官方手册建议"延迟SW注册直至初始化页面完成加载"。如果终端代码可控,可以预先创建webview注册service worker,真正使用时webview自动从本地存储中初始化SW。
更新策略
更新分为内核自动更新以及页面手动更新。
- 页面手动更新。Service Worker规范提供了skipWaiting以及update两种方式可以让开发者更新SW。具体代码以及问题的解决见《ServiceWorker上篇:应用与实践》。
- 内核自动更新。以下任何一个条件都会触发sw.js更新。
- scope内的页面跳转
- 24小时有效期之后,functional events例如push、sync事件会再次触发更新(跳过HTTP cache)。
- register另一个service worker URL。
退出策略
以下引用Service Worker Draft对退出策略的描述。可见退出时机是不确定的。
A user agent may terminate service workers at any time it:
- Has no event to handle.
- Detects abnormal operation: such as infinite loops and tasks exceeding imposed time limits (if any) while handling the events.
在内核中更新以及退出策略具体是由ServiceWorkerVersion实现。它记录了SW start_time_(request开始时间)、stop_time_(进入STOPPING状态)、idle_time_(空闲时间,大于30s则退出SW)、stale_time_(过期时间,用于判断是否需要更新SW),并且持有timeout_timer_(检查以及更新SW状态,间隔30s触发一次)、update_timer_(触发SW脚本更新,一次性)。由timeout_timer_触发执行ServiceWorkerVersion::OnTimeoutTimer检查以上时间是否过期、是否还存在request、Webkit端embedded_worker是否正常而决定更新或者退出SW。
存在的问题:
- SW状态不明确,导致业务逻辑混乱,例如《上篇》提到的跨scope context通信随时中断以及更新后新旧sw.js兼容问题。
解决方法:
方法一:提示用户刷新页面,在用户体验与开发成本上做权衡。实现方案见https://zhuanlan.zhihu.com/p/51118741。
方法二:开发者可以通过监听SW声明周期来维护scope页面以及sw.js的业务逻辑,但是会带来额外的开发负担。具体在《上篇》有描述。
网络资源
初始化webview时:
- WebViewChromiumFactoryProvider.startChromiumLocked(java)
{初始化AwBrowserContext时会初始化StoragePartitionImpl
在StoragePartitionImplMap::Get中会初始化全局的RequestContext,设置ServiceWorkerRequestInterceptor作为网络请求的拦截器,在请求时会先执行ServiceWorkerRequestInterceptor::MaybeInterceptRequest}
Content层开始网络请求:
- ResourceDispatcherHostImpl::ContinuePendingBeginRequest
{构造URLRequest参数以及不同类型的RequestHandler,设置在UserData中,然后开始网络请求} - ServiceWorkerProviderHost::CreateRequestHandler
{判断是否能用SW,如果是sw.js Context里的请求创建ServiceWorkerContextRequestHandler;如果是网页Context则创建ServiceWorkerControlleeRequestHandler。并设置在URLRequest的UserData中。
判断是否能用SW的条件是是否设置skip_service_worker、是否存在ServiceWorker(provider_host以及version)、URL Origin是否可以走SW条件进行判断。}
net层创建请求任务时:
- ServiceWorkerRequestInterceptor::MaybeInterceptRequest
{如果UserData存在ServiceWorkerRequestHandler,则调用具体实现子类的MaybeCreateJob创建网络任务。}
1.1 ServiceWorkerContextRequestHandler::MaybeCreateJob
{sw.js内请求。如果Version内script_cache_map存在缓存,则创建ServiceWorkerReadFromCacheJob,直接从缓存中读取;不存在则创建ServiceWorkerWriteToCacheJob并发起正常的URLRequest->Start(),OnResponseStarted后写入缓存。}
1.2 ServiceWorkerControlleeRequestHandler::MaybeCreateJob
{页面请求。创建ServiceWorkerURLRequestJob并设置response_type是走SW、正常网络、还是返回Render端请求。
请求的是主资源,则在storage中找Registration并判断是否需要更新以及启动browser端的SW,一切正常则走SW;异常则走正常网络。
请求子资源。如果sw.js注册了fetch事件,则走SW;否则走正常网络或返回Render端。} - ServiceWorkerURLRequestJob::StartRequest
2.1 走正常网络。调URLRequest::Restart重新创建Job执行。
2.2 走Render。返回400状态码。
2.3 走SW。创建ServiceWorkerFetchDispatcher,触发Webkit端fetch事件通知sw.js(如果Webkit端没起SW,则起来之后再触发)。 - ServiceWorkerGlobalScopeProxy::dispatchFetchEvent
{构造JS的Request以及FetchEvent对象,调EventTarget::dispatchEvent执行sw.js的fetch回调。}
sw.js调用fetch流程:
- FetchManager::fetch
{JS接口层。URL安全检查,performHTTPFetch中构造ResourceRequest以及WorkerThreadableLoader,发起请求} - WorkerThreadableLoader::start
{将执行从worker线程抛到WebEmbeddedWorkerImpl webview的主线程} - DocumentThreadableLoader::start
{通过RawResource::fetch异步或RawResource::fetchSynchronously同步请求资源,收到资源回调后抛回worker线程处理} - FetchManager::Loader::didReceiveResponse
{构造JS的Response对象并回调sw.js}
存在的问题:
SW提供开发者管理网络资源的能力,让开发者可以根据业务更好地优化浏览体验。但是使用SW的同时也不可避免地引入额外的逻辑,这些overhead影响有多大?这里以先取缓存再网络更新为例说明。
实验环境:代码如下。硬件还是ARM64位四核1.3G的设备。
数据:无SW情况下。第一次请求耗时1500ms,同样资源第二次请求(返回304)220ms。
已经启动SW情况下。第一次请求耗时2800ms,其中发起Fetch耗时40ms,二次发起请求耗时170ms,二次fetch网络请求耗时2600ms。同样资源第二次请求耗时30ms。
结论:排除实际网络请求时间(网速波动),SW额外的耗时,第一次请求多出210ms(40+170)左右。但是第二次同样资源请求耗时只要30ms。可见初始化后,SW消息通知以及Caches的额外耗时在10ms的数量级。
请求耗时 = FinishRequest - BeginRequest
发起Fetch耗时 = onFetch - BeginRequest
二次发起请求耗时 = fetchPromise - onFetch
二次fetch网络请求 = fetchPromiseFinish - fetchPromise
// sw.js
self.addEventListener('fetch', function(event) {
console.log('onFetch', Date.now());
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return cache.match(event.request).then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
console.log('fetchPromiseFinish', Date.now());
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
console.log('fetchPromise', Date.now());
return response || fetchPromise;
})
})
);
});
// index.js
console.log('BeginRequest', Date.now());
fetch('xxx.js').then(()=>{ console.log('FinishRequest', Date.now()}; );
总结:
Service Worker提供了网页后台服务能力,是Progressive Web App的重要组成部分,如同Native App各类Service服务的集成。但是跟Native不一样的是,Service Worker存在多进程线程通信、需要V8 JIT执行脚本、依赖网络、全局单例、跨平台兼容性等问题需要解决。随着W3C标准以及技术的发展,希望Service Worker甚至PWA能做到真正的跨平台应用开发。想要系统性了解PWA最新进展可见以下链接。https://w3c.github.io/web-roadmaps/mobile/