ServiceWorker下篇:blink內(nèi)核實現(xiàn)原理

承接《ServiceWorker上篇:應用與實踐》

本文內(nèi)容

本文先從整體架構(gòu)闡述各個模塊的定位,再從生命周期赃额、請求網(wǎng)絡(luò)資源兩個流程研究service worker在內(nèi)核的實現(xiàn)原理以及性能數(shù)據(jù)绪颖。(以下內(nèi)容基于chromium 57版本)

整體架構(gòu)

這里從模塊的粒度剖析SW整體實現(xiàn)架構(gòu)欠母,關(guān)鍵類的具體職責見大圖。

  1. Webkit模塊分為兩層:
  • JS接口層仔戈。以idl方式與對應V8 Object進行綁定关串,提供Context以及ServiceWorker拧廊、Cache等對象供service-worker.js使用。主要負責對接V8接口以及CSP安全檢查晋修。
  • Web實現(xiàn)層吧碾。分為兩部分,一是作為代理傳遞JS接口層到底層content/child模塊的調(diào)用墓卦;二是管理SW需要用到Webkit模塊的資源倦春,例如WebEmbeddedWorkerImpl負責創(chuàng)建一個webview加載service-worker.js,創(chuàng)建workerThread請求網(wǎng)絡(luò)資源落剪。
  1. content/child模塊睁本。負責轉(zhuǎn)發(fā)IPC消息,Webkit與browser的中間層忠怖,運行于render主線程呢堰。ServiceWorkerNetworkProvider用于RenderFrameImpl資源請求時添加provider id標識SW類型。

  2. content/render模塊凡泣。負責轉(zhuǎn)發(fā)IPC消息枉疼,Webkit與browser的中間層,運行于render worker線程鞋拟。

  3. browser模塊分為兩層:

  • SW對外接口層骂维,負責在browser端提供SW能力或調(diào)用其他模塊能力。其中包含管理SW生命周期以及攔截網(wǎng)絡(luò)請求兩部分贺纲,控制SW實現(xiàn)層航闺。
  • SW實現(xiàn)層,負責SW具體業(yè)務猴誊,實現(xiàn)W3C標準来颤。包括注冊狀態(tài)、service-worker.js版本管理稠肘、本地disk cache存儲福铅、SW進程或線程(取決于平臺)創(chuàng)建等。與Web實現(xiàn)層通過render間接通信项阴。

由此可見滑黔,service worker標準關(guān)鍵的實現(xiàn)邏輯是Web實現(xiàn)層以及SW實現(xiàn)層,分別實現(xiàn)browser端以及WebKit端功能环揽。

生命周期

注冊流程

1.ServiceWorkerContainer::registerServiceWorker
{JS入口略荡,檢查是否https、sw.js域名安全歉胶、是否與host一致汛兜、CSP檢查}
2.ServiceWorkerDispatcher::RegisterServiceWorker
{發(fā)送IPC到host端}
3.ServiceWorkerDispatcherHost::OnRegisterServiceWorker
{browser端安全檢查}
4.ServiceWorkerJobCoordinator::Register
{創(chuàng)建register job,push進隊列立即執(zhí)行}
5.ServiceWorkerRegisterJob::Start
{判斷是注冊還是更新通今,檢查storage是否已有registration粥谬。
如果第一次注冊肛根,則創(chuàng)建ServiceWorkerRegistration并保存在ServiceWorkerProviderHost中,并且創(chuàng)建Version漏策,調(diào)用StartWorker派哲。
結(jié)束后設(shè)置狀態(tài),異步回調(diào)js register函數(shù)的ResolvePromise掺喻,返回registration芭届。
IPC通知ServiceWorkerGlobalScope::dispatchExtendableEvent觸發(fā)Install事件}
6.ServiceWorkerVersion::StartWorker
{停止更新sw.js的定時器。(注意sw.js的更新策略在此類實現(xiàn))
判斷SW狀態(tài)感耙,如果是STOPPED則調(diào)用EmbeddedWorkerInstance啟動service worker褂乍。}
7.EmbeddedWorkerInstance::StartTask::Start
{在browser UI線程以script_url創(chuàng)建RenderProcessHost進程。如果有則復用即硼,沒有則通過SiteInstance::CreateForURL創(chuàng)建進程逃片。
在IO線程通過render接口層調(diào)用WebEmbeddedWorkerImpl::startWorkerContext創(chuàng)建Webkit端service worker。}
9.EmbeddedWorkerDispatcher::StartWorkerContext
{準備啟動數(shù)據(jù)谦絮,例如scriptURL, userAgent, v8CacheOptions等题诵。
創(chuàng)建WebView以及WebLocalFrame洁仗。用FrameLoader加載scriptURL域名的空頁面(shadow page)层皱。}
10.EmbeddedWorkerDispatcher::didFinishDocumentLoad
{創(chuàng)建ServiceWorkerNetworkProvider用于攔截網(wǎng)絡(luò)請求以及控制host生命周期。
創(chuàng)建WorkerScriptLoader在worker線程異步拉取并加載scriptURL資源赠潦。}
11.EmbeddedWorkerDispatcher::onScriptLoaderFinished
{啟動service worker線程叫胖。準備啟動數(shù)據(jù)例如IndexedDB, ServiceWorkerGlobalScope等client以及各種settings, scriptURL資源內(nèi)容,創(chuàng)建ServiceWorkerThread并執(zhí)行scriptURL的內(nèi)容她奥。釋放之前拉取scriptURL的WorkerScriptLoader瓮增。}

存在的問題:
1.注冊耗時。從代碼路徑分析可以看到哩俭,第一次注冊需要從Webkit端IPC到browser端再IPC回到Webkit绷跑,其中還需要在UI、IO凡资、worker線程中切換砸捏,加載webview。在ARM64位四核1.3G隙赁,內(nèi)存2G的Android設(shè)備上垦藏,實測register到成功回調(diào)的執(zhí)行時間是1.1s左右(排除網(wǎng)絡(luò)拉取頁面以及腳本時間);而重啟blink內(nèi)核伞访,第二次打開網(wǎng)頁register耗時只需要30ms掂骏,耗時差異的原因是第一次注冊成功后,SW相關(guān)信息以及腳本會保存在本地厚掷,第二次加載scope網(wǎng)頁時會讀取本地信息初始化SW弟灼,register時在browser端發(fā)現(xiàn)已存在ServiceWorkerVersion則直接返回级解。
解決方法:對于首次注冊耗時的問題,google官方手冊建議"延遲SW注冊直至初始化頁面完成加載"袜爪。如果終端代碼可控蠕趁,可以預先創(chuàng)建webview注冊service worker,真正使用時webview自動從本地存儲中初始化SW辛馆。

更新策略

更新分為內(nèi)核自動更新以及頁面手動更新俺陋。

  1. 頁面手動更新。Service Worker規(guī)范提供了skipWaiting以及update兩種方式可以讓開發(fā)者更新SW昙篙。具體代碼以及問題的解決見《ServiceWorker上篇:應用與實踐》腊状。
  2. 內(nèi)核自動更新。以下任何一個條件都會觸發(fā)sw.js更新苔可。
  • scope內(nèi)的頁面跳轉(zhuǎn)
  • 24小時有效期之后缴挖,functional events例如push、sync事件會再次觸發(fā)更新(跳過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.

在內(nèi)核中更新以及退出策略具體是由ServiceWorkerVersion實現(xiàn)。它記錄了SW start_time_(request開始時間)湾蔓、stop_time_(進入STOPPING狀態(tài))瘫析、idle_time_(空閑時間,大于30s則退出SW)默责、stale_time_(過期時間贬循,用于判斷是否需要更新SW),并且持有timeout_timer_(檢查以及更新SW狀態(tài)桃序,間隔30s觸發(fā)一次)杖虾、update_timer_(觸發(fā)SW腳本更新,一次性)媒熊。由timeout_timer_觸發(fā)執(zhí)行ServiceWorkerVersion::OnTimeoutTimer檢查以上時間是否過期奇适、是否還存在request、Webkit端embedded_worker是否正常而決定更新或者退出SW泛释。

存在的問題:

  1. SW狀態(tài)不明確滤愕,導致業(yè)務邏輯混亂,例如《上篇》提到的跨scope context通信隨時中斷以及更新后新舊sw.js兼容問題怜校。
    解決方法:
    方法一:提示用戶刷新頁面间影,在用戶體驗與開發(fā)成本上做權(quán)衡。實現(xiàn)方案見https://zhuanlan.zhihu.com/p/51118741茄茁。
    方法二:開發(fā)者可以通過監(jiān)聽SW聲明周期來維護scope頁面以及sw.js的業(yè)務邏輯魂贬,但是會帶來額外的開發(fā)負擔巩割。具體在《上篇》有描述。

網(wǎng)絡(luò)資源

初始化webview時:

  1. WebViewChromiumFactoryProvider.startChromiumLocked(java)
    {初始化AwBrowserContext時會初始化StoragePartitionImpl
    在StoragePartitionImplMap::Get中會初始化全局的RequestContext付燥,設(shè)置ServiceWorkerRequestInterceptor作為網(wǎng)絡(luò)請求的攔截器宣谈,在請求時會先執(zhí)行ServiceWorkerRequestInterceptor::MaybeInterceptRequest}

Content層開始網(wǎng)絡(luò)請求:

  1. ResourceDispatcherHostImpl::ContinuePendingBeginRequest
    {構(gòu)造URLRequest參數(shù)以及不同類型的RequestHandler,設(shè)置在UserData中键科,然后開始網(wǎng)絡(luò)請求}
  2. ServiceWorkerProviderHost::CreateRequestHandler
    {判斷是否能用SW闻丑,如果是sw.js Context里的請求創(chuàng)建ServiceWorkerContextRequestHandler;如果是網(wǎng)頁Context則創(chuàng)建ServiceWorkerControlleeRequestHandler勋颖。并設(shè)置在URLRequest的UserData中嗦嗡。
    判斷是否能用SW的條件是是否設(shè)置skip_service_worker、是否存在ServiceWorker(provider_host以及version)饭玲、URL Origin是否可以走SW條件進行判斷侥祭。}

net層創(chuàng)建請求任務時:

  1. ServiceWorkerRequestInterceptor::MaybeInterceptRequest
    {如果UserData存在ServiceWorkerRequestHandler,則調(diào)用具體實現(xiàn)子類的MaybeCreateJob創(chuàng)建網(wǎng)絡(luò)任務茄厘。}
    1.1 ServiceWorkerContextRequestHandler::MaybeCreateJob
    {sw.js內(nèi)請求矮冬。如果Version內(nèi)script_cache_map存在緩存,則創(chuàng)建ServiceWorkerReadFromCacheJob次哈,直接從緩存中讀忍ナ稹;不存在則創(chuàng)建ServiceWorkerWriteToCacheJob并發(fā)起正常的URLRequest->Start()亿乳,OnResponseStarted后寫入緩存硝拧。}
    1.2 ServiceWorkerControlleeRequestHandler::MaybeCreateJob
    {頁面請求径筏。創(chuàng)建ServiceWorkerURLRequestJob并設(shè)置response_type是走SW葛假、正常網(wǎng)絡(luò)、還是返回Render端請求滋恬。
    請求的是主資源聊训,則在storage中找Registration并判斷是否需要更新以及啟動browser端的SW,一切正常則走SW恢氯;異常則走正常網(wǎng)絡(luò)带斑。
    請求子資源。如果sw.js注冊了fetch事件勋拟,則走SW勋磕;否則走正常網(wǎng)絡(luò)或返回Render端。}
  2. ServiceWorkerURLRequestJob::StartRequest
    2.1 走正常網(wǎng)絡(luò)敢靡。調(diào)URLRequest::Restart重新創(chuàng)建Job執(zhí)行挂滓。
    2.2 走Render。返回400狀態(tài)碼啸胧。
    2.3 走SW赶站。創(chuàng)建ServiceWorkerFetchDispatcher幔虏,觸發(fā)Webkit端fetch事件通知sw.js(如果Webkit端沒起SW,則起來之后再觸發(fā))贝椿。
  3. ServiceWorkerGlobalScopeProxy::dispatchFetchEvent
    {構(gòu)造JS的Request以及FetchEvent對象想括,調(diào)EventTarget::dispatchEvent執(zhí)行sw.js的fetch回調(diào)。}

sw.js調(diào)用fetch流程:

  1. FetchManager::fetch
    {JS接口層烙博。URL安全檢查瑟蜈,performHTTPFetch中構(gòu)造ResourceRequest以及WorkerThreadableLoader,發(fā)起請求}
  2. WorkerThreadableLoader::start
    {將執(zhí)行從worker線程拋到WebEmbeddedWorkerImpl webview的主線程}
  3. DocumentThreadableLoader::start
    {通過RawResource::fetch異步或RawResource::fetchSynchronously同步請求資源渣窜,收到資源回調(diào)后拋回worker線程處理}
  4. FetchManager::Loader::didReceiveResponse
    {構(gòu)造JS的Response對象并回調(diào)sw.js}

存在的問題:
SW提供開發(fā)者管理網(wǎng)絡(luò)資源的能力踪栋,讓開發(fā)者可以根據(jù)業(yè)務更好地優(yōu)化瀏覽體驗。但是使用SW的同時也不可避免地引入額外的邏輯图毕,這些overhead影響有多大夷都?這里以先取緩存再網(wǎng)絡(luò)更新為例說明。
實驗環(huán)境:代碼如下予颤。硬件還是ARM64位四核1.3G的設(shè)備囤官。
數(shù)據(jù):無SW情況下。第一次請求耗時1500ms蛤虐,同樣資源第二次請求(返回304)220ms党饮。
已經(jīng)啟動SW情況下。第一次請求耗時2800ms驳庭,其中發(fā)起Fetch耗時40ms刑顺,二次發(fā)起請求耗時170ms,二次fetch網(wǎng)絡(luò)請求耗時2600ms饲常。同樣資源第二次請求耗時30ms蹲堂。
結(jié)論:排除實際網(wǎng)絡(luò)請求時間(網(wǎng)速波動),SW額外的耗時贝淤,第一次請求多出210ms(40+170)左右柒竞。但是第二次同樣資源請求耗時只要30ms〔ゴ希可見初始化后朽基,SW消息通知以及Caches的額外耗時在10ms的數(shù)量級。

請求耗時 = FinishRequest - BeginRequest
發(fā)起Fetch耗時 = onFetch - BeginRequest
二次發(fā)起請求耗時 = fetchPromise - onFetch
二次fetch網(wǎng)絡(luò)請求 = 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()}; );

總結(jié):

Service Worker提供了網(wǎng)頁后臺服務能力离陶,是Progressive Web App的重要組成部分稼虎,如同Native App各類Service服務的集成。但是跟Native不一樣的是招刨,Service Worker存在多進程線程通信霎俩、需要V8 JIT執(zhí)行腳本、依賴網(wǎng)絡(luò)、全局單例茸苇、跨平臺兼容性等問題需要解決排苍。隨著W3C標準以及技術(shù)的發(fā)展,希望Service Worker甚至PWA能做到真正的跨平臺應用開發(fā)学密。想要系統(tǒng)性了解PWA最新進展可見以下鏈接淘衙。https://w3c.github.io/web-roadmaps/mobile/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市腻暮,隨后出現(xiàn)的幾起案子彤守,更是在濱河造成了極大的恐慌,老刑警劉巖哭靖,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件具垫,死亡現(xiàn)場離奇詭異,居然都是意外死亡试幽,警方通過查閱死者的電腦和手機筝蚕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铺坞,“玉大人起宽,你說我怎么就攤上這事〖谜ィ” “怎么了坯沪?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長擒滑。 經(jīng)常有香客問我腐晾,道長,這世上最難降的妖魔是什么丐一? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任藻糖,我火速辦了婚禮,結(jié)果婚禮上钝诚,老公的妹妹穿的比我還像新娘颖御。我一直安慰自己榄棵,他們只是感情好凝颇,可當我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疹鳄,像睡著了一般拧略。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瘪弓,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天垫蛆,我揣著相機與錄音,去河邊找鬼。 笑死袱饭,一個胖子當著我的面吹牛川无,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虑乖,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼懦趋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了疹味?” 一聲冷哼從身側(cè)響起仅叫,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糙捺,沒想到半個月后诫咱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡洪灯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年坎缭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片签钩。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡幻锁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出边臼,到底是詐尸還是另有隱情哄尔,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布柠并,位于F島的核電站岭接,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏臼予。R本人自食惡果不足惜鸣戴,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粘拾。 院中可真熱鬧窄锅,春花似錦、人聲如沸缰雇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽械哟。三九已至疏之,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暇咆,已是汗流浹背锋爪。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工丙曙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人其骄。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓亏镰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拯爽。 傳聞我的和親對象是個殘疾皇子拆挥,可洞房花燭夜當晚...
    茶點故事閱讀 43,566評論 2 349

推薦閱讀更多精彩內(nèi)容