承接《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)鍵類的具體職責見大圖。
- 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ò)資源落剪。
content/child模塊睁本。負責轉(zhuǎn)發(fā)IPC消息,Webkit與browser的中間層忠怖,運行于render主線程呢堰。ServiceWorkerNetworkProvider用于RenderFrameImpl資源請求時添加provider id標識SW類型。
content/render模塊凡泣。負責轉(zhuǎn)發(fā)IPC消息枉疼,Webkit與browser的中間層,運行于render worker線程鞋拟。
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)核自動更新以及頁面手動更新俺陋。
- 頁面手動更新。Service Worker規(guī)范提供了skipWaiting以及update兩種方式可以讓開發(fā)者更新SW昙篙。具體代碼以及問題的解決見《ServiceWorker上篇:應用與實踐》腊状。
- 內(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泛释。
存在的問題:
- 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時:
- WebViewChromiumFactoryProvider.startChromiumLocked(java)
{初始化AwBrowserContext時會初始化StoragePartitionImpl
在StoragePartitionImplMap::Get中會初始化全局的RequestContext付燥,設(shè)置ServiceWorkerRequestInterceptor作為網(wǎng)絡(luò)請求的攔截器宣谈,在請求時會先執(zhí)行ServiceWorkerRequestInterceptor::MaybeInterceptRequest}
Content層開始網(wǎng)絡(luò)請求:
- ResourceDispatcherHostImpl::ContinuePendingBeginRequest
{構(gòu)造URLRequest參數(shù)以及不同類型的RequestHandler,設(shè)置在UserData中键科,然后開始網(wǎng)絡(luò)請求} - 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)建請求任務時:
- 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端。} - 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ā))贝椿。 - ServiceWorkerGlobalScopeProxy::dispatchFetchEvent
{構(gòu)造JS的Request以及FetchEvent對象想括,調(diào)EventTarget::dispatchEvent執(zhí)行sw.js的fetch回調(diào)。}
sw.js調(diào)用fetch流程:
- FetchManager::fetch
{JS接口層烙博。URL安全檢查瑟蜈,performHTTPFetch中構(gòu)造ResourceRequest以及WorkerThreadableLoader,發(fā)起請求} - WorkerThreadableLoader::start
{將執(zhí)行從worker線程拋到WebEmbeddedWorkerImpl webview的主線程} - DocumentThreadableLoader::start
{通過RawResource::fetch異步或RawResource::fetchSynchronously同步請求資源渣窜,收到資源回調(diào)后拋回worker線程處理} - 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/