WKWebView 加載生命周期與代理方法剖析
1. 前言
從 WebView 開始加載一條請求,到頁面完整呈現這一過程發生了什么?無論是做 WebView 性能優化還是異常問題監控與排查,我們都離不開對這一問題的思考與探索。
2. iOS端WebKit加載框架

三大進程間通信關系圖
如上圖所示,說明了 UIProcess、WebContent、NetworkProcess 三大進程間的通信關系,并列舉了他們的主要職責。本系列的源碼剖析工作始終圍繞三大進程,對其作進一步說明:
NetworkProcess進程:主要負責網絡請求加載,所有的網頁共享這一進程。與原生網絡請求開發一致,NetworkProcess 也是通過封裝的 NSURLSession 發起并管理網絡請求的。但不同的是,這一過程中有較多的網絡進度的回調工作以及各類網絡協議管理,比如資源緩存協議、HSTS 協議、cookie 管理協議等。
WebContent進程:主要負責頁面資源的管理,包含前進后退歷史,pageCache,頁面資源的解析、渲染。并把該進程中的各類事件通過代理方式通知給 UIProcess。
UIProcess進程:主要負責與 WebContent 進行交互,與 APP 在同一進程中,可以進行 WebView 的功能配置,并接收來自 WebContent 進程的各類消息,配合業務代碼執行任務的決策,例如是否發起請求,是否接受響應等。
理解了三大進程的主要工作職責后,接下來,我們首先結合三大進程描述 WebKit 從網絡加載到渲染的全流程,讓讀者對網頁加載有一個宏觀上的理解。
3. iOS端WebKit加載流程
我們使用如下方法,從 UIProcess 層通過 loadReqeust 方法發起頁面加載請求(此處 request 只能是 get 請求,如果配置為 post 請求,WebKit 內核基于性能考慮,在跨進程傳輸時,會將 body 數據丟棄,導致異常)。
[self.webView loadRequest:request];通過跟蹤 WebKit 源碼,我們提取核心步驟如下:
UIProcess 中的 loadRequest 首先會觸發 NetworkProcess 進程創建,然后通過進程間通信的方式將 request 發送給 NetworkProcess 進程進行 preconnect 預鏈接操作,通過網絡三次握手建立 TCP 鏈接,以便加快后續網絡資源請求速度。
UIProcess 通過進程間通信的方式將 request 發送給 WebContent 進程,WebContent 進程創建 DocumentLoader 加載器加載網絡請求,并取消上個頁面的所有還在加載的請求,然后通過字典綁定當前頁面ID與創建好的 NetworkProcss 進程(便于服務端數據返回時,查找數據回填所對應的頁面),最終將請求交付給 NetworkProcess 中的 NSURLSession 進行處理。
NetworkProcess 通過 NSURLSession 復用之前 preconnect 預鏈接,繼續進行網絡加載,此時等待網絡請求返回,網絡層會繼續將數據通過進程間通信方式傳輸給 WebContent 進程進行處理,開始流式進行數據解析,一邊接收一邊處理,進行詞法分析、語法分析,并在這一過程中加載解析出來的 js、css、圖片、字體等子資源,最終動態的生成(DOM 樹與 CSSOM 樹合成)渲染樹,在這一過程中,每次接受到新數據導致渲染樹有變更后,就會觸發一次 checkAndDispatchDidReachVisuallyNonEmptyState 方法,檢查當前頁面是否達到上屏狀態,若達到上屏狀態就進行上屏渲染。
達到上屏狀態的條件如下:
如果返回的 data 是普通文本文字,或返回的數據中包含普通文本文字,那只需要達到非空200字節即可以觸發上屏渲染;
如果返回的 data 是圖片資源類,則判斷像素大小 > 32*32,即可觸發上屏渲染;
如果不滿足以上條件,對于主文檔,判斷后面是否繼續接收數據,如果不繼續,則觸發上屏渲染;如后續還有數據,則循環上述流程直至觸發上屏。渲染完成,整個加載過程結束。

WebKit加載流程
對首屏渲染感興趣的同學可以嘗試配合服務端來針對部分場景(例如文字、圖片)做一些數據分包優化,或許會有一些不錯的收獲。
在描述完網頁核心加載過程后,為了更貼近我們日常的開發工作,接下來我們將重點描述以上工作流程如何與 UIProcess 進程(APP 進程)關聯起來。
4. 加載生命周期代理方法
丨4.1 WKNavigationDelegate 方法簡要介紹
-
@protocol WKNavigationDelegate <NSObject>
-
-
-
@optional
-
// 請求之前,決定是否要跳轉:用戶點擊網頁上的鏈接,需要打開新頁面時,將先調用這個方法。
-
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
-
-
-
// 頁面開始加載時調用
-
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
-
-
-
// 接收到響應數據后,決定是否跳轉
-
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
-
-
-
// 主機地址被重定向時調用
-
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
-
-
-
// 當開始加載主文檔數據失敗時調用
-
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
-
-
-
// 當內容開始返回時調用
-
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
-
-
-
// 頁面加載完畢時調用
-
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
-
-
-
// 當主文檔已committed時,如果發生錯誤將進行調用
-
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
-
-
-
// 如果需要證書驗證,進行驗證,一般使用默認證書策略即可
-
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
-
-
-
// 9.0才能使用,web內容處理中斷時會觸發,可針對該情況進行reload操作,可解決部分白屏問題
-
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
-
-
-
@end
以上是 WKNavigationDelegate 代理方法及蘋果官方接口描述,足夠應付日常的開發工作了,但細節描述上有些粗糙,不能讓我們寫出踏實放心的代碼,因此我們需要徹底理解這些方法背后的運行邏輯。接下來,我們將結合實踐與源碼調試對重點方法進行剖析。
丨4.2 深入理解 WKNavigationDelegate 方法

WKNavigationDelegate代理方法調用流程
如上圖所示,描述了 WKNavigationDelegate 代理方法的調用流程,我們將在該圖的基礎上,重點描述帶顏色標注的代理方法,如下:
1)decidePolicyForNavigationAction 剖析
如第3節講述的網頁加載流程,當 WebContent 即將創建 DocumentLoader 加載器時,會首先觸發 decidePolicyForNavigationAction 代理方法。如果我們選擇 cancel ,那么瀏覽內核會完全忽略這一操作,后續也不再繼續執行其他操作,我們可以放心的使用 cancel 取消掉我們不想加載的主文檔請求,而無需擔憂任何異常。但當我們選擇 alllow 后,我們會進入一個稍微復雜的邏輯判斷,內核代碼首先判斷該該鏈接是否是 universalLink 類型的鏈接,如果判斷是 universalLink 類型的鏈接,會嘗試去調起三方 app,如果能調起,則會 cancel 當前請求,否則才會走到正常的網絡加載邏輯(如果需要統計 universalLink 調起情況與或建設屏蔽能力,可以再仔細閱讀該處源碼)。
2)didStartProvisionalNavigation 理解
decidePolicyForNavigationAction 方法中選擇 allow 并且判斷為非 universalLink 鏈接后,會立即觸發 didStartProvisionalNavigation 方法,表示即將開始加載主文檔。這個方法看似只是對 decidePolicyForNavigationAction 方法的確認,但是值得思考的問題是方法名中的 Provisional 究竟是什么意思。其實,頁面開始頁面加載后為了更好的區分加載的各階段,會將網絡加載的初始階段命名為臨時狀態,此時的頁面是不會記入歷史的,直到接收到首個數據包,才會對當前頁面進行 committed 提交,并觸發didCommitNavigation 方法通知 UIProcess 進程該事件,同時將網絡 data 提交給 WebContent 進行渲染樹生成。我們可由此引申出下一個問題,即 didFailProvisionalNavigation 與 didFailNavigation 的關系。
3)didFailProvisionalNavigation 與 didFailNavigation 的分別在什么時候執行?他們之間有什么關系?
當 NetworkProcess 進程發生網絡錯誤時,錯誤首先由 NSURLSession 回調到 WebContent 層。WebContent 會判斷當前主文檔加載狀態,如果處于臨時態,則錯誤會回調給 didFailProvisionalNavigation 方法;如果處于提交態,則錯誤會回調給 didFailNavigation 方法。

主文檔加載狀態圖
4)didFinishNavigation 究竟什么時候執行?與頁面上屏是否有關?
在上面的描述中,我們已經理解了 NetworkProcess 層也是使用 NSURLSession 加載主文檔的。當 NSURLSession 接收到 finish 事件時,會將該消息通過進程通信方式傳遞給 WebContent 進程,WebContent 進程再傳遞給 UIProcess 進程,直到被我們的代理方法響應。因此 didFinishNavigation 在 NSURLSession 的網絡加載結束時就會觸發,但因為跨了兩次進程通信,因此對比網絡層,實際上是有一定的延遲的。與子資源加載和頁面上屏無時間先后關系。
5. Tips
一定要緊密結合三大進程去理解 WebKit 源碼,形成基于進程的知識體系。
可以直接修改源碼驗證猜想。例如在驗證觸發渲染條件時,可以在源碼中禁止網絡層 didfinish 事件執行,并自己構造數據返回,驗證各類上屏觸發條件。