WKWebView 是 iOS 中重要的网页组成部分,我们需要通过 WKWebView 框架来与 WebKit 打交道,虽然官方提供了 WKWebView 的各种 API,但是我们经常会遇到各种各样的问题,这个时候就需要对于WKWebView 的底层 WebKit 有一定的了解了.

带着问题来看

有过一些 iOS 的网络拦截的人应该知道一个事情:”NSURLProtocol 可以拦截 app 里的网络请求却拦截不到webview里面 post 的 body 内容”,那么原因是什么?带着这个问题来切入研究 webkit.

NSURLProtocol 是如何拦截 App 的网络请求的

具体的代码实现不做详细讨论,简单的说一下原理.

系统里有一个 Protocol 列表,每次网络发起请求时,系统说按顺序查询这个列表并获得处理权限.处理之后接着可以正常的转发请求给系统网络.

简单的总结:NSURLProtocol 是 iOS 网络层的 钩子机制,让你可以在任何网络请求发出前拦截它,并完全自定义如何处理这个请求,而上层调用者完全无感知。

webview 的网络请求

在这里就开始基于 WebKit-7622.1.16 版本的源码,来进行分析.

WKWebView 采用了多进程的机制.分为以下几种:

进程种类 职责 数量
UIProcess 应用逻辑、API 1 个
WebProcess 渲染、JS 执行 每个页面 1 个
NetworkProcess 所有网络请求 1 个
GPUProcess 图形渲染(可选) 1 个

通常情况下,加载一个网页,是这样的流程(不考虑 GPUProcess):

  1. UI Process: “加载 https://example.com”
  2. Web Process: 准备渲染环境
  3. Network Process: 发起 HTTP 请求
  4. 收到响应数据
  5. Web Process: 解析 HTML → 构建 DOM → 布局 → 绘制
  6. UI Process: 显示渲染结果

在这里,可以的得知,在 WKWebView 内部,是由一个 整体的 Network Process 来控制网球请求的,而传递数据则通过跨进程 IPC 进行传递.

这里我们要分清一种情况,假如你是在 App 内部点击某个链接进入 WebView,比如类似的的代码

  NSURL *url = [NSURL URLWithString:@"https://example.com"]; │
  NSURLRequest *request = [NSURLRequest requestWithURL:url]; │
 [webView loadRequest:request]; 

则这个网络请求本身是可以被 NSURLProtocol 所拦截,因为它本质上是 App 内部的一个网络请求,是发生在 App 的进程中的.

而如果已经进入 WebView 之后再进行的网络请求,则完全是绕过了 App 进程,直接进行Web Process → NetworkProcess的通信.

而在这里就无法使用 NSURLProtocol 进行拦截了,在 webkit 的相关代码中有对应解释

    // We don't send HTTP body over IPC for better performance.
    // Also, it's not always possible to do, as streams can only be created in process that does networking.
    if ([requestToSerialize HTTPBody] || [requestToSerialize HTTPBodyStream]) {
        auto mutableRequest = adoptNS([requestToSerialize mutableCopy]);
        [mutableRequest setHTTPBody:nil];
        [mutableRequest setHTTPBodyStream:nil];
        requestToSerialize = WTFMove(mutableRequest);
    }

这里可以很明显的看到,如果请求包含 HTTPBody 或 HTTPBodyStream ,就创建一个可变副本并清空这两个字段;这样之后的数据只会包含 URL、header、method 等元数据.

之所以这样做,和 webkit 的多进程设计有关.

在多个进程中,是通过跨进程 IPC 进程数据传递的,而这个数据传递要经过“序列化→拷贝→反序列化”的操作,数据量一旦变多,开销会显著放大.

并且 TTPBodyStream 还是一个实时性非常强的数据,它往往是一次性、不可重用的输入流,跨进程复制之后无法保证读取的一致性.

况且还可以保证架构隔离的安全,将网络请求真正发送的位置固定在 NetworkProcess 里,集中控制网络权限,统一处理安全策略、缓存、证书等网络逻辑.