ServiceWorker下篇:blink内核实现原理

承接《ServiceWorker上篇:应用与实践》

本文内容

本文先从整体架构阐述各个模块的定位,再从生命周期、请求网络资源两个流程研究service worker在内核的实现原理以及性能数据。(以下内容基于chromium 57版本)

整体架构

这里从模块的粒度剖析SW整体实现架构,关键类的具体职责见大图。

  1. 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请求网络资源。
  1. content/child模块。负责转发IPC消息,Webkit与browser的中间层,运行于render主线程。ServiceWorkerNetworkProvider用于RenderFrameImpl资源请求时添加provider id标识SW类型。

  2. content/render模块。负责转发IPC消息,Webkit与browser的中间层,运行于render worker线程。

  3. 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。

更新策略

更新分为内核自动更新以及页面手动更新。

  1. 页面手动更新。Service Worker规范提供了skipWaiting以及update两种方式可以让开发者更新SW。具体代码以及问题的解决见《ServiceWorker上篇:应用与实践》。
  2. 内核自动更新。以下任何一个条件都会触发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。

存在的问题:

  1. SW状态不明确,导致业务逻辑混乱,例如《上篇》提到的跨scope context通信随时中断以及更新后新旧sw.js兼容问题。
    解决方法:
    方法一:提示用户刷新页面,在用户体验与开发成本上做权衡。实现方案见https://zhuanlan.zhihu.com/p/51118741
    方法二:开发者可以通过监听SW声明周期来维护scope页面以及sw.js的业务逻辑,但是会带来额外的开发负担。具体在《上篇》有描述。

网络资源

初始化webview时:

  1. WebViewChromiumFactoryProvider.startChromiumLocked(java)
    {初始化AwBrowserContext时会初始化StoragePartitionImpl
    在StoragePartitionImplMap::Get中会初始化全局的RequestContext,设置ServiceWorkerRequestInterceptor作为网络请求的拦截器,在请求时会先执行ServiceWorkerRequestInterceptor::MaybeInterceptRequest}

Content层开始网络请求:

  1. ResourceDispatcherHostImpl::ContinuePendingBeginRequest
    {构造URLRequest参数以及不同类型的RequestHandler,设置在UserData中,然后开始网络请求}
  2. ServiceWorkerProviderHost::CreateRequestHandler
    {判断是否能用SW,如果是sw.js Context里的请求创建ServiceWorkerContextRequestHandler;如果是网页Context则创建ServiceWorkerControlleeRequestHandler。并设置在URLRequest的UserData中。
    判断是否能用SW的条件是是否设置skip_service_worker、是否存在ServiceWorker(provider_host以及version)、URL Origin是否可以走SW条件进行判断。}

net层创建请求任务时:

  1. 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端。}
  2. ServiceWorkerURLRequestJob::StartRequest
    2.1 走正常网络。调URLRequest::Restart重新创建Job执行。
    2.2 走Render。返回400状态码。
    2.3 走SW。创建ServiceWorkerFetchDispatcher,触发Webkit端fetch事件通知sw.js(如果Webkit端没起SW,则起来之后再触发)。
  3. ServiceWorkerGlobalScopeProxy::dispatchFetchEvent
    {构造JS的Request以及FetchEvent对象,调EventTarget::dispatchEvent执行sw.js的fetch回调。}

sw.js调用fetch流程:

  1. FetchManager::fetch
    {JS接口层。URL安全检查,performHTTPFetch中构造ResourceRequest以及WorkerThreadableLoader,发起请求}
  2. WorkerThreadableLoader::start
    {将执行从worker线程抛到WebEmbeddedWorkerImpl webview的主线程}
  3. DocumentThreadableLoader::start
    {通过RawResource::fetch异步或RawResource::fetchSynchronously同步请求资源,收到资源回调后抛回worker线程处理}
  4. 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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容