首页 - IOS编程

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 源码,我们提取核心步骤如下:

  1. UIProcess 中的 loadRequest 首先会触发 NetworkProcess 进程创建,然后通过进程间通信的方式将 request 发送给 NetworkProcess 进程进行 preconnect 预链接操作,通过网络三次握手建立 TCP 链接,以便加快后续网络资源请求速度。

  2. UIProcess 通过进程间通信的方式将 request 发送给 WebContent 进程,WebContent 进程创建 DocumentLoader 加载器加载网络请求,并取消上个页面的所有还在加载的请求,然后通过字典绑定当前页面ID与创建好的 NetworkProcss 进程(便于服务端数据返回时,查找数据回填所对应的页面),最终将请求交付给 NetworkProcess 中的 NSURLSession 进行处理。

  3. NetworkProcess 通过 NSURLSession 复用之前 preconnect 预链接,继续进行网络加载,此时等待网络请求返回,网络层会继续将数据通过进程间通信方式传输给 WebContent 进程进行处理,开始流式进行数据解析,一边接收一边处理,进行词法分析、语法分析,并在这一过程中加载解析出来的 js、css、图片、字体等子资源,最终动态的生成(DOM 树与 CSSOM 树合成)渲染树,在这一过程中,每次接受到新数据导致渲染树有变更后,就会触发一次 checkAndDispatchDidReachVisuallyNonEmptyState 方法,检查当前页面是否达到上屏状态,若达到上屏状态就进行上屏渲染。

达到上屏状态的条件如下:

  1. 如果返回的 data 是普通文本文字,或返回的数据中包含普通文本文字,那只需要达到非空200字节即可以触发上屏渲染;

  2. 如果返回的 data 是图片资源类,则判断像素大小 > 32*32,即可触发上屏渲染;

  3. 如果不满足以上条件,对于主文档,判断后面是否继续接收数据,如果不继续,则触发上屏渲染;如后续还有数据,则循环上述流程直至触发上屏。渲染完成,整个加载过程结束。

WebKit加载流程

对首屏渲染感兴趣的同学可以尝试配合服务端来针对部分场景(例如文字、图片)做一些数据分包优化,或许会有一些不错的收获。

在描述完网页核心加载过程后,为了更贴近我们日常的开发工作,接下来我们将重点描述以上工作流程如何与 UIProcess 进程(APP 进程)关联起来。

4. 加载生命周期代理方法

4.1 WKNavigationDelegate 方法简要介绍

  1. @protocol WKNavigationDelegate <NSObject>
  2. @optional
  3. // 请求之前,决定是否要跳转:用户点击网页上的链接,需要打开新页面时,将先调用这个方法。
  4. - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
  5. // 页面开始加载时调用
  6. - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
  7. // 接收到响应数据后,决定是否跳转
  8. - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
  9. // 主机地址被重定向时调用
  10. - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
  11. // 当开始加载主文档数据失败时调用
  12. - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
  13. // 当内容开始返回时调用
  14. - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
  15. // 页面加载完毕时调用
  16. - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
  17. // 当主文档已committed时,如果发生错误将进行调用
  18. - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
  19. // 如果需要证书验证,进行验证,一般使用默认证书策略即可
  20. - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
  21. // 9.0才能使用,web内容处理中断时会触发,可针对该情况进行reload操作,可解决部分白屏问题
  22. - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
  23. @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 事件执行,并自己构造数据返回,验证各类上屏触发条件。