从 WebView 开始加载一条请求,到页面完整呈现这一过程发生了什么?无论是做 WebView 性能优化还是异常问题监控与排查,我们都离不开对这一问题的思考与探索。
在本篇文章中,我们将在《深入理解 WKWebView(入门篇)—— WebKit 源码调试与分析》 的基础上,结合 iOS 端 WKWebView 的 WKNavigationDelegate 代理方法,站在移动端的视角深入分析 WKWebView 网络请求加载的生命周期流程,给大家提供更多业务上的思路。
在《深入理解 WKWebView(入门篇)—— WebKit 源码调试与分析》中我们通过 Demo 验证了 WebKit 三大进程工作模型,并简述了三大进程的主要职责。接下来我们将展开描述下其中的一些细节。
三大进程间通信关系图
如上图所示,说明了 UIProcess、WebContent、NetworkProcess 三大进程间的通信关系,并列举了他们的主要职责。本系列的源码剖析工作始终围绕三大进程,对其作进一步说明:
理解了三大进程的主要工作职责后,接下来,我们首先结合三大进程描述 WebKit 从网络加载到渲染的全流程,让读者对网页加载有一个宏观上的理解。
我们使用如下方法,从 UIProcess 层通过 loadReqeust 方法发起页面加载请求(此处 request 只能是 get 请求,如果配置为 post 请求,WebKit 内核基于性能考虑,在跨进程传输时,会将 body 数据丢弃,导致异常)。
[self.webView loadRequest:request];
通过跟踪 WebKit 源码,我们提取核心步骤如下:
达到上屏状态的条件如下:
WebKit加载流程
对首屏渲染感兴趣的同学可以尝试配合服务端来针对部分场景(例如文字、图片)做一些数据分包优化,或许会有一些不错的收获。
在描述完网页核心加载过程后,为了更贴近我们日常的开发工作,接下来我们将重点描述以上工作流程如何与 UIProcess 进程(APP 进程)关联起来。
@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 代理方法及苹果官方接口描述,足够应付日常的开发工作了,但细节描述上有些粗糙,不能让我们写出踏实放心的代码,因此我们需要彻底理解这些方法背后的运行逻辑。接下来,我们将结合实践与源码调试对重点方法进行剖析。
WKNavigationDelegate代理方法调用流程
如上图所示,描述了 WKNavigationDelegate 代理方法的调用流程,我们将在该图的基础上,重点描述带颜色标注的代理方法,如下:
如第3节讲述的网页加载流程,当 WebContent 即将创建 DocumentLoader 加载器时,会首先触发 decidePolicyForNavigationAction 代理方法。如果我们选择 cancel ,那么浏览内核会完全忽略这一操作,后续也不再继续执行其他操作,我们可以放心的使用 cancel 取消掉我们不想加载的主文档请求,而无需担忧任何异常。但当我们选择 alllow 后,我们会进入一个稍微复杂的逻辑判断,内核代码首先判断该该链接是否是 universalLink 类型的链接,如果判断是 universalLink 类型的链接,会尝试去调起三方 app,如果能调起,则会 cancel 当前请求,否则才会走到正常的网络加载逻辑(如果需要统计 universalLink 调起情况与或建设屏蔽能力,可以再仔细阅读该处源码)。
decidePolicyForNavigationAction 方法中选择 allow 并且判断为非 universalLink 链接后,会立即触发 didStartProvisionalNavigation 方法,表示即将开始加载主文档。这个方法看似只是对 decidePolicyForNavigationAction 方法的确认,但是值得思考的问题是方法名中的 Provisional 究竟是什么意思。其实,页面开始页面加载后为了更好的区分加载的各阶段,会将网络加载的初始阶段命名为临时状态,此时的页面是不会记入历史的,直到接收到首个数据包,才会对当前页面进行 committed 提交,并触发didCommitNavigation 方法通知 UIProcess 进程该事件,同时将网络 data 提交给 WebContent 进行渲染树生成。我们可由此引申出下一个问题,即 didFailProvisionalNavigation 与 didFailNavigation 的关系。
当 NetworkProcess 进程发生网络错误时,错误首先由 NSURLSession 回调到 WebContent 层。WebContent 会判断当前主文档加载状态,如果处于临时态,则错误会回调给 didFailProvisionalNavigation 方法;如果处于提交态,则错误会回调给 didFailNavigation 方法。
主文档加载状态图
在上面的描述中,我们已经理解了 NetworkProcess 层也是使用 NSURLSession 加载主文档的。当 NSURLSession 接收到 finish 事件时,会将该消息通过进程通信方式传递给 WebContent 进程,WebContent 进程再传递给 UIProcess 进程,直到被我们的代理方法响应。因此 didFinishNavigation 在 NSURLSession 的网络加载结束时就会触发,但因为跨了两次进程通信,因此对比网络层,实际上是有一定的延迟的。与子资源加载和页面上屏无时间先后关系。
以上是本文梳理的 WKWebView 加载生命周期与代理方法剖析。因篇幅有限,本文没有直接把大量的源码放上来逐行解释,很多网络加载的策略细节也没有呈现出来,但相信读者通过本文的整体框架分析和源码追踪思路,结合 WebKit 源码自己调试,一定能了解更多自己感兴趣的细节功能的原理,形成自己的知识体系。后面我们将继续从独特的视角分析 WKWebView 里那些有趣的功能细节,希望总有一块知识能帮助大家最终将好的想法落实在业务里,助力实际开发。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8