WKWebView 的性能優(yōu)化
起因
隨著移動設(shè)備性能不斷增強,web 頁面的性能體驗逐漸變得可以接受,又因為 web 開發(fā)模式的諸多好處(跨平臺,動態(tài)更新,減體積揖铜,無限擴展)贿肩,APP 客戶端里出現(xiàn)越來越多內(nèi)嵌 web 頁面)物邑,很多 APP 把一些功能模塊改成用 H5 實現(xiàn)茬射。
雖然說 H5 頁面性能變好了,但如果沒針對性地做一些優(yōu)化,體驗還是很糟糕的,主要兩部分體驗:
- 頁面啟動白屏時間:打開一個 H5 頁面需要做一系列處理簿煌,會有一段白屏時間,體驗糟糕夺荒。
- 響應(yīng)流暢度:由于 webkit 的渲染機制瞒渠,單線程,歷史包袱等原因技扼,頁面刷新/交互的性能體驗不如原生伍玖。
由于以上原因,公司準備從第一點入手剿吻,做 webview 的優(yōu)化項目(達到秒開 webview )窍箍。因為UIWebView
在 iOS12 就被標記廢棄了,所以決定先從WKWebView
入手研究。
思路
webview 加載過程
打開一個頁面的過程有很多優(yōu)化點仔燕,包括前端和客戶端造垛,常規(guī)的前端和后端的性能優(yōu)化已有前輩們總結(jié)過最佳實踐,主要的是:
- 降低請求量:合并資源晰搀,減少 HTTP 請求數(shù)五辽,minify / gzip 壓縮,webP外恕,lazyLoad杆逗。
- 加快請求速度:預解析DNS,減少域名數(shù)鳞疲,并行加載罪郊,CDN 分發(fā)。
- 緩存:HTTP 協(xié)議緩存請求尚洽,離線緩存 manifest悔橄,離線數(shù)據(jù)緩存 localStorage。
- 渲染:JS/CSS優(yōu)化腺毫,加載順序癣疟,服務(wù)端渲染模板直出。
- 客戶端:預請求web所需數(shù)據(jù)
大家可以看出潮酒,主要是第三階段之前睛挚,用戶看到的頁面一直處于白屏。首先要優(yōu)化的就是這段時間急黎。
打開一個頁面的過程有很多優(yōu)化點扎狱,包括前端和客戶端,常規(guī)的前端和后端的性能優(yōu)化已有前輩們總結(jié)過最佳實踐勃教,主要的是:
下面只講客戶端優(yōu)化部分:
減少第一階段耗時
- 在使用前預先初始化好 webView淤击,從而減小耗時。
- 在初始化的同時荣回,通過 Native 來完成一些網(wǎng)絡(luò)請求等過程遭贸,使得 webView 初始化不是完全的阻塞后續(xù)過程。
- webview 池心软,可以用兩個或多個 webview 重復使用,而不是每次打開 H5 都新建 webview著蛙。
減少第二階段耗時
- 離線包
- 預先下載離線包删铃,可以達到立即展示的效果。
- 離線包可以很方便地根據(jù)版本做增量更新踏堡。
- 離線包以壓縮包的方式下發(fā)猎唁,同時會經(jīng)過加密和校驗,防止運營商和第三方對其劫持篡改顷蟆。
- 數(shù)據(jù)緩存
- 第一次打開會有延遲诫隅,但是后續(xù)打開就會很快
- 可以自己控制緩存腐魂,方便管理
- 客戶端代替請求
- 客戶端可以在網(wǎng)絡(luò)請求上做像 DNS 預解析/ IP 直連/長連接/并行請求等更細致的優(yōu)化
難點
方案是通用的,不區(qū)分 UIWebView 和 WKWebView逐纬,但是目前很少有以 WKWebView 為目標的方案蛔屹,那么以上技術(shù)方案在 WKWebView 中實現(xiàn)有什么難點呢?
難點在 NSURLProtocol
WKWebView 無法使用 NSURLProtocol 攔截 http 請求
這個問題網(wǎng)上早有方案:
[WKBrowsingContextController registerSchemeForCustomProtocol:@"schemes"];
WKWebView 使用 NSURLProtocol 攔截后豁生,HTTPBody的數(shù)據(jù)會丟失
從網(wǎng)上克隆了 webkit 進行編譯調(diào)試兔毒,嘗試解決 Body 丟失的問題(體驗到了啥叫大型項目的編譯速度)
從圖上重點標注的地方可以看到:
WKWebView 的網(wǎng)絡(luò)請求是在另外一個進程中操作的,然后如果 app 主進程需要攔截請求的話甸箱,通過 XPC 來進行兩個進程間的通信育叁。
蘋果出于性能或其他考慮,會在給主進程的 URLProtocol 傳輸請求時將
HTTPBody
和HTTPBodyStream
置為 nil 芍殖。
源代碼
嘗試解決方案:
- 使用 runtime 黑魔法豪嗽,在其將 HTTPBody 置為 nil 之前,先保存下來豌骏?
因為網(wǎng)絡(luò)請求是在其他進程中操作昵骤,沒有辦法在主進程使用 runtime 進行攔截。也就是說在 app 中決定攔截 http 請求的那一刻起肯适,攔截到的請求注定是沒有 HTTPBody 的变秦。 - 使用 任何方式進入到 Networking 進程做一些操作 ?
嘗試了 Mac 端的 XPC demo框舔,XPC 的回調(diào)是在各自進程蹦玫,是不能操作其他進程的。 - 在
HTTPBody
置為 nil 之前刘绣,是否會有代碼走到主進程樱溉,然后拿到 request 進行操作?
抱歉纬凤,經(jīng)過測試福贞,在HTTPBody
置為 nil 之前,主進程不會收到關(guān)于 request 的調(diào)用
解決
難到就沒有任何方法解決了么停士?無意中看到一個特別有趣的想法又點燃了我的希望挖帘。
既然 Networking 進程會將 HTTPBody
置為 nil ,那我要做的就是兩點:
1. 不讓其置為 nil
2. 或者在其置為 nil 之前恋技,先將 HTTPBody
保存下來
第一點:上面已經(jīng)嘗試失斈匆ā;第二點:在 native 端也嘗試失敗蜻底,那在 H5 側(cè)做保存操作呢骄崩?
要攔截的是 H5 的請求,那說明 H5 側(cè)肯定是知道請求參數(shù)的。
嘗試 H5 與 native 結(jié)合來解決 HTTPBody
丟失問題
H5 發(fā)起發(fā)起請求有三種方式:
1. Form
2. XMLHttpRequest
3. Fetch
Fetch
是在 iOS10 以后支持的要拂,從通用場景看抠璃,只需要處理 Form
和 XMLHttpRequest
發(fā)起的帶 HTTPBody
的請求就可以.
基本所有 H5 開發(fā)者,肯定知道 H5 里面也有黑魔法脱惰,就是原型:
XMLHttpRequest
對應(yīng)的是 XMLHttpRequest.prototype.send
方法
Form
對應(yīng)的是 HTMLFormElement.prototype.submit
方法
我們對以上方法使用 WKUserScript
在 WKUserScriptInjectionTimeAtDocumentStart
時機做對應(yīng)攔截搏嗡。這樣 H5 在發(fā)起請求前,先將 POST 的數(shù)據(jù)發(fā)送給 native 存儲(WKScriptMessageHandler
)枪芒。然后在 native 攔截到匹配到的請求彻况,嘗試接管,并重新設(shè)置 HTTPBody
舅踪,而且由于攔截到的是 request 纽甘,只需要補齊HTTPBody
,其他在 h5 中原本對 request 做的各種操作也是存在的抽碌,這樣就能解決問題了
這個方法提供了一種解決HTTPBody
丟失問題的可能悍赢,并且大部分 app,使用應(yīng)該完全夠用货徙。本人已經(jīng)按照上述方案實現(xiàn)左权,并接入到 app 中,在解決了一些細節(jié)問題后痴颊,將各個流程中的 H5 頁面走了一遍赏迟,目前沒有發(fā)現(xiàn)不支持的請求。
參考:
WebView性能蠢棱、體驗分析與優(yōu)化
移動 H5 首屏秒開優(yōu)化方案探討
IMYWebLoader
VasSonic