Webview加載H5優化小記

行到水窮處,坐看雲起時css

原文連接html

1、概述

一、背景
  • 鑑於H5的優點,客戶端的不少業務都由H5來實現,Webview成了App中H5業務的惟一載體。
  • WebView組件是iOS組件體系中很是重要的一個,以前的UIWebView 存在嚴重的性能和內存消耗問題,iOS 8以後推出WKWebView,旨在代替UIWebView;
  • WKWebView在性能、穩定性、內存佔用上有很大的提高,支持更多的HTML5特性,高達60fps的滾動刷新率以及內置手勢;能夠經過KVO監控網絡加載的進度,獲取網頁title;
  • 實踐中,大部分App的H5業務將由WKWebview承載。
二、H5頁面的體驗問題

從用戶角度,相比Native頁面,H5頁面的體驗問題主要有兩點:前端

  • 頁面打開時間慢:打開一個 H5 頁面須要作一系列處理,會有一段白屏時間,體驗糟糕。
  • 響應流暢度較差:因爲 WebKit 的渲染機制,單線程,歷史包袱等緣由,頁面刷新/交互的性能體驗不如原生。

這裏討論的是:第一點,怎樣減小白屏時間。web

2、Webview打開H5

經過Webview打開H5頁面,請求並獲得 HTML、CSS 和 JavaScript 等資源並對其進行處理從而渲染出 Web 頁面。後端

一、加載流程
  • 初始化Webview -> 請求頁面 -> 下載數據 -> 解析HTML -> 請求 js/css 資源 ->DOM 渲染 -> 解析 JS 執行 -> JS 請求數據 -> 解析渲染 -> 下載渲染圖片-> 頁面完整展現

H5頁面加載流程.png

  • DOM渲染以前耗時主要在兩部分:初始化Webview數據請求,通常Webview首次初始化在400ms這個量級,二次加載能少一個量級。
  • 數據請求依賴網絡,網絡請求通常通過:DNS查詢、TCP 鏈接、HTTP 請求和響應。數據包括HTML、JS和CSS資源,這些都是在webview在loadRequest:以後作的,這一階段,用戶所見到的都是白屏。(雖然4G已經成爲主流,可是4G延遲明顯高於Wifi)。
二、H5頁面渲染

對H5頁面的渲染,主要包括:渲染樹構建、佈局及繪製,具體可分爲:瀏覽器

  • 處理 HTML 標記並構建 DOM 樹。緩存

  • 處理 CSS 標記並構建 CSSOM(CSS Object Model) 樹。性能優化

  • 將 DOM 與 CSSOM 合併成一個渲染樹。bash

  • 根據渲染樹來佈局,以計算每一個節點的幾何信息。網絡

  • 將各個節點繪製到屏幕上。

說明:這五個步驟並不必定一次性順序完成。若是 DOM 或 CSSOM 被修改,以上過程須要重複執行,這樣才能計算出哪些像素須要在屏幕上進行從新渲染。實際頁面中,CSS 與 JavaScript 每每會屢次修改 DOM 和 CSSOM。具體參考:DOM渲染機制與常見性能優化

三、總結
  • 分析Webview打開H5打開的過程,咱們發現,在H5優化中,前端重任在肩;
下降請求量:合併資源,減小 HTTP 請求數,minify / gzip 壓縮,webP,lazyLoad。
加快請求速度:預解析DNS,減小域名數,並行加載,CDN 分發。
緩存:HTTP 協議緩存請求,離線緩存 manifest,離線數據緩存localStorage。
渲染:JS/CSS優化,加載順序,服務端渲染,pipeline。
複製代碼
  • 可是客戶端也很重要,主要優化DOM渲染以前這些事情,能夠作有:減小DNS時間預初始化WebView 以及 HTML、JS、CSS等資源離線下載
  • 列舉在某業務中筆者實踐過的比較trick的優化方案,而後再引出筆者認爲理想的方案。

2、WebView的客戶端優化(trick版)

因爲是接入第三方的H5頁面,接入離線包方案,須要比較繁雜的商務溝通和技術挑戰(業務邏輯和代碼超級詭異),臨時採用以下優化方案

一、預加載資源
  • 將首頁面須要的JS文件CSS文件等資源放在一個URL地址(和業務url同域名);
  • 啓動App後,間隔X秒去加載;加載的策略是,檢查當前和上一次間隔時間,超時則加載,有效期忽略預加載請求。
二、預初始化Webview
  • 首次初始化Webview,須要初始化瀏覽器內核,須要的時間在400ms這個量級;二次初始化時間在幾十ms這個量級;

  • 根據此特徵:選擇在APP 啓動後X秒,預建立(初始化)一個 Webview 而後釋放,這樣等使用到 H5 模塊,再加載 Webview時,加載時間也少了很多。

  • 結合步驟一中預加載公共資源,也須要Webview,因此選擇在加載公共資源包時候,首次初始化Webview,加載資源,而後釋放。

三、最終方案(無可奈何)

​ 因爲第三方業務H5不少問題,和人力上不足;不得不須要客戶端強行配合優化,在產品的要求下,不得不採用以下方案,方案的前提是:業務H5儘量少修改,甚至不修改,客戶端還要保證首屏加載快;

  • 預加載資源
  • 預建立Webview並加載首頁H5,駐留在內存中,須要的時候,馬上顯示。
四、方案的後遺症
  • 我不建議這種trick作法,由於自從開了這個口子,後續不少H5需求不走以前既定的離線包方案,在內存中預建立多個Webview (最多4個),加載H5時候不用新建Webview,從Webview池中獲取;
  • 此種Webview池方案帶來諸多隱患:內存壓力、詭異的白屏、JS形成的內存泄露,頁面的清空等等問題(填坑填到掉頭髮)。

3、離線包方案

一、概述
  • 離線包方案纔是業務主流的H5加載優化方案,很是建議在客戶端團隊和前端團隊推廣,相似預建立Webview加載H5不該該成爲主流。

  • 將每一個獨立的H5功能模塊,相關HTML、Javascript、CSS 等頁面內靜態資源打包到一個壓縮包內,客戶端能夠下載該離線包到本地,而後打開Webview,直接從本地加載離線包,從而最大程度地擺脫網絡環境對 H5 頁面的影響。

  • 離線包能夠提高用戶體驗(頁面加載更快),還能夠實現動態更新(在推出新版本或是緊急發佈的時候,能夠把修改的資源放入離線包,經過更新配置讓應用自動下載更新)

二、方案描述

引用bang的離線包方案,簡單描述以下

  • 後端使用構建工具把同一個業務模塊相關的頁面和資源打包成一個文件,同時對文件加密/簽名。

  • 客戶端根據配置表,在自定義時機去把離線包拉下來,作解壓/解密/校驗等工做。

  • 根據配置表,打開某個業務時轉接到打開離線包的入口頁面。

  • 攔截網絡請求,對於離線包已經有的文件,直接讀取離線包數據返回,不然走 HTTP 協議緩存邏輯。

  • 離線包更新時,根據版本號後臺下發兩個版本間的 diff 數據,客戶端合併,增量更新。

說明:目前WKWebView已經能成爲主流,可是WKWebView在實現離線包方案時,攔截網絡請求有坑。

三、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解決上述問題了。

四、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。

4、其餘

一、LocalWebServer
  • 離線包方案中,除了攔截請求加載資源的方式,還有種在項目中搭建local web server,用以得到本地資源。市面有比較完善的框架
CocoaHttpServer (支持iOS、macOS及多種網絡場景)
GCDWebServer (基於iOS,不支持 https 及 webSocket)
Telegraph (Swift實現,功能較上面兩類更完善)
複製代碼
二、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;
三、推薦資料
相關文章
相關標籤/搜索