一、概述
1、背景
- 鑒于H5的優勢,客戶端的很多業務都由H5來實現,Webview成了App中H5業務的唯一載體。
- WebView組件是IOS組件體系中非常重要的一個,之前的UIWebView 存在嚴重的性能和內存消耗問題,iOS 8之后推出WKWebView,旨在代替UIWebView;
- WKWebView在性能、穩定性、內存占用上有很大的提升,支持更多的html5特性,高達60fps的滾動刷新率以及內置手勢;可以通過KVO監控網絡加載的進度,獲取網頁title;
- 實踐中,大部分App的H5業務將由WKWebview承載。
2、H5頁面的體驗問題
從用戶角度,相比Native頁面,H5頁面的體驗問題主要有兩點:
- 頁面打開時間慢:打開一個 H5 頁面需要做一系列處理,會有一段白屏時間,體驗糟糕。
- 響應流暢度較差:由于 WebKit 的渲染機制,單線程,歷史包袱等原因,頁面刷新/交互的性能體驗不如原生。
這里討論的是:第一點,怎樣減少白屏時間。
二、Webview打開H5
通過Webview打開H5頁面,請求并得到 HTML、css 和 JAVAScript 等資源并對其進行處理從而渲染出 Web 頁面。
1、加載流程
- 初始化Webview -> 請求頁面 -> 下載數據 -> 解析HTML -> 請求 js/css 資源 ->DOM 渲染 -> 解析 JS 執行 -> JS 請求數據 -> 解析渲染 -> 下載渲染圖片-> 頁面完整展示
- DOM渲染之前耗時主要在兩部分:初始化Webview 和 數據請求,一般Webview首次初始化在400ms這個量級,二次加載能少一個量級。
- 數據請求依賴網絡,網絡請求一般經過:DNS查詢、TCP 連接、HTTP 請求和響應。數據包括HTML、JS和CSS資源,這些都是在webview在loadRequest:之后做的,這一階段,用戶所見到的都是白屏。(雖然4G已經成為主流,但是4G延遲明顯高于Wifi)。
2、H5頁面渲染
對H5頁面的渲染,主要包括:渲染樹構建、布局及繪制,具體可分為:
- 處理 HTML 標記并構建 DOM 樹。
- 處理 CSS 標記并構建 CSSOM(CSS Object Model) 樹。
- 將 DOM 與 CSSOM 合并成一個渲染樹。
- 根據渲染樹來布局,以計算每個節點的幾何信息。
- 將各個節點繪制到屏幕上。
說明:這五個步驟并不一定一次性順序完成。如果 DOM 或 CSSOM 被修改,以上過程需要重復執行,這樣才能計算出哪些像素需要在屏幕上進行重新渲染。實際頁面中,CSS 與 JavaScript 往往會多次修改 DOM 和 CSSOM。具體參考:DOM渲染機制與常見性能優化
3、總結
- 分析Webview打開H5打開的過程,我們發現,在H5優化中,前端重任在肩;
降低請求量:合并資源,減少 HTTP 請求數,minify / gzip 壓縮,webP,lazyLoad。 加快請求速度:預解析DNS,減少域名數,并行加載,CDN 分發。 緩存:HTTP 協議緩存請求,離線緩存 manifest,離線數據緩存localStorage。 渲染:JS/CSS優化,加載順序,服務端渲染,pipeline。 復制代碼
- 但是客戶端也很重要,主要優化DOM渲染之前這些事情,可以做有:減少DNS時間、預初始化WebView 以及 HTML、JS、CSS等資源離線下載。
- 列舉在某業務中筆者實踐過的比較trick的優化方案,然后再引出筆者認為理想的方案。
二、WebView的客戶端優化(trick版)
由于是接入第三方的H5頁面,接入離線包方案,需要比較繁雜的商務溝通和技術挑戰(業務邏輯和代碼超級詭異),臨時采用如下優化方案。
1、預加載資源
- 將首頁面需要的JS文件和CSS文件等資源放在一個URL地址(和業務url同域名);
- 啟動App后,間隔X秒去加載;加載的策略是,檢查當前和上一次間隔時間,超時則加載,有效期忽略預加載請求。
2、預初始化Webview
- 首次初始化Webview,需要初始化瀏覽器內核,需要的時間在400ms這個量級;二次初始化時間在幾十ms這個量級;
- 根據此特征:選擇在APP 啟動后X秒,預創建(初始化)一個 Webview 然后釋放,這樣等使用到 H5 模塊,再加載 Webview時,加載時間也少了不少。
- 結合步驟一中預加載公共資源,也需要Webview,所以選擇在加載公共資源包時候,首次初始化Webview,加載資源,然后釋放。
3、最終方案(迫不得已)
?由于第三方業務H5很多問題,和人力上不足;不得不需要客戶端強行配合優化,在產品的要求下,不得不采用如下方案,方案的前提是:業務H5盡可能少修改,甚至不修改,客戶端還要保證首屏加載快;
- 預加載資源
- 預創建Webview并加載首頁H5,駐留在內存中,需要的時候,立刻顯示。
4、方案的后遺癥
- 我不建議這種trick做法,因為自從開了這個口子,后續很多H5需求不走之前既定的離線包方案,在內存中預創建多個Webview (最多4個),加載H5時候不用新建Webview,從Webview池中獲取;
- 此種Webview池方案帶來諸多隱患:內存壓力、詭異的白屏、JS造成的內存泄露,頁面的清空等等問題(填坑填到掉頭發)。
三、離線包方案
1、概述
- 離線包方案才是業務主流的H5加載優化方案,非常建議在客戶端團隊和前端團隊推廣,類似預創建Webview加載H5不應該成為主流。
- 將每個獨立的H5功能模塊,相關HTML、Javascript、CSS 等頁面內靜態資源打包到一個壓縮包內,客戶端可以下載該離線包到本地,然后打開Webview,直接從本地加載離線包,從而最大程度地擺脫網絡環境對 H5 頁面的影響。
- 離線包可以提升用戶體驗(頁面加載更快),還可以實現動態更新(在推出新版本或是緊急發布的時候,可以把修改的資源放入離線包,通過更新配置讓應用自動下載更新)
2、方案描述
引用bang的離線包方案,簡單描述如下:
- 后端使用構建工具把同一個業務模塊相關的頁面和資源打包成一個文件,同時對文件加密/簽名。
- 客戶端根據配置表,在自定義時機去把離線包拉下來,做解壓/解密/校驗等工作。
- 根據配置表,打開某個業務時轉接到打開離線包的入口頁面。
- 攔截網絡請求,對于離線包已經有的文件,直接讀取離線包數據返回,否則走 HTTP 協議緩存邏輯。
- 離線包更新時,根據版本號后臺下發兩個版本間的 diff 數據,客戶端合并,增量更新。
說明:目前WKWebView已經能成為主流,但是WKWebView在實現離線包方案時,攔截網絡請求有坑。
3、WKWebView攔截網絡請求的坑
- 雖然NSURLProtocol可以攔截監聽每一個URL Loading System中發出request請求,記住是URL Loading System中那些類發出的請求,也支持AFNetwoking,UIWebView發出的request,NSURLProtocol都可以攔截和監聽。
- 因為WKWebView 在獨立進程里執行網絡請求。一旦注冊 http(s) scheme 后,網絡請求將從 Network Process 發送到 App Process,這樣 NSURLProtocol 才能攔截網絡請求。
- 但是在 WebKit2 的設計里使用 MessageQueue 進行進程之間的通信,Network Process 會將請求 encode 成一個 Message,然后通過 IPC(進程間通信) 發送給 App Process。出于性能的原因,encode 的時候 將HTTPBody 和 HTTPBodyStream 這兩個字段丟棄掉(坑)
- 因此,如果通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme, 那么由 WKWebView 發起的所有 http(s)請求都會通過 IPC 傳給主進程 NSURLProtocol 處理,導致 post 請求 body 被清空;
//蘋果開源的 WebKit2 源碼暴露了私有API: + [WKBrowsingContextController registerSchemeForCustomProtocol:] //通過注冊 http(s) scheme 后 WKWebView 將可以使用 NSURLProtocol 攔截 http(s) 請求: Class cls = NSClassFromString(@"WKBrowsingContextController”); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([(id)cls respondsToSelector:sel]) { // 注冊http(s) scheme, 把 http和https請求交給 NSURLProtocol處理 [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; } 復制代碼
**說明1:**名目張膽使用私有API,是過不了AppStore審核的,具體使用什么辦法,想來你也懂(hun xiao)。
說明2:一旦打開ATS開關:Allow Arbitrary Loads 選項設置為NO,通過 registerSchemeForCustomProtocol 注冊了 http(s) scheme,WKWebView 發起的所有 http(s) 網絡請求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項設置為YES);
說明3:iOS11之后可以通過WKURLSchemeHandler去完成對WKWebView的請求攔截,不需要再調用私有API解決上述問題了。
4、WKWebView自定義資源scheme
- 向WKWebView 注冊 customScheme, 比如 dynamic://, 而不是https或http,避免對https或http請求的影響
- 保證使用離線包功能的請求,沒有post方式,遇到customScheme請求,比如dynamic://www.dynamicalbumlocalimage.com/,通過 NSURLProtocol 攔截這個請求并加載離線數據。
- iOS 11上, WebKit 提供的WKURLSchemeHandler可實現攔截,需要注意的只允許開發者攔截自定義 Scheme 的請求,不允許攔截 “http”、“https”、“ftp”、“file” 等的請求,否則會crash。
四、其他
1、LocalWebServer
- 離線包方案中,除了攔截請求加載資源的方式,還有種在項目中搭建local web server,用以獲得本地資源。市面有比較完善的框架
CocoaHttpServer (支持iOS、macOS及多種網絡場景) GCDWebServer (基于iOS,不支持 https 及 webSocket) Telegraph (Swift實現,功能較上面兩類更完善) 復制代碼
- 具體可參考 基于 LocalWebServer 實現 WKWebView 離線資源加載, 之前團隊有過實踐,采用的是GCDWebServer
2、WKWebView loadRequest 問題
- 在 WKWebView 上通過 loadRequest 發起的 post 請求 body 數據會丟失:
//同樣是由于進程間通信性能問題,HTTPBody字段被丟棄 [request setHTTPMethod:@"POST"]; [request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]]; [wkwebview loadRequest: request]; 復制代碼
解決:假如想通過-[WKWebView loadRequest:]加載 post 請求 (原始請求)request1: h5.nanhua.com/order/list,可以通過以下步驟實現:
- 替換請求 scheme,生成新的 post 請求 request2: post://h5.nanhua.com/order/list, 同時將 request1 的 body 字段復制到 request2 的 header 中(WebKit 不會丟棄 header 字段);
- 通過-[WKWebView loadRequest:] 加載新的 post 請求 request2;
- 并且通過 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注冊 scheme: post://;
- 注冊 NSURLProtocol 攔截請求 post://h5.nanhua.com/order/list ,替換請求 scheme, 生成新的請求 request3: h5.nanhua.com/order/list,將 request2 header的body 字段復制到 request3 的 body 中,并使用 NSURLSession 加載 request3,最后將加載結果返回 WKWebView;
3、推薦資料
- 移動端本地 H5 秒開方案探索與實現
- 使用 PageSpeed Insights 進行移動版分析
- WebView性能、體驗分析與優化
- iOS app秒開H5優化總結
- 賦予H5以Native的生命 ——《WebView優化》