1 minute read

WKWebView 是 iOS 中最重要的网页容器之一。日常开发里,我们通常通过它提供的 API 使用 Web 能力;但一旦遇到请求拦截、Cookie、POST Body、进程通信这类问题,只停留在 API 层往往是不够的,这时就需要进一步理解它背后的 WebKit 架构。

这篇文章不打算泛泛介绍 WebKit,而是从一个经典现象切入:为什么 NSURLProtocol 能拦截 App 中的普通网络请求,却拿不到 WKWebView 中某些请求的 POST body

从一个经典问题切入

做过 iOS 网络拦截的同学,往往会遇到这样一个现象:

NSURLProtocol 可以拦截 App 里的普通网络请求,却拿不到 WKWebView 中某些 POST 请求的 body

那么,原因到底是什么?带着这个问题,我们来初步理解 WKWebView 背后的多进程网络架构。

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

具体实现这里不展开,只简单说明一下原理。

URL Loading System 中,系统会根据已注册的 NSURLProtocol 子类来判断一个请求是否应该交给自定义协议处理;如果命中,对应的协议对象就可以接管这个请求,或者在处理后再转发给系统网络栈。

简单来说,NSURLProtocol 是 iOS 网络层的一种 钩子机制:它允许你在请求发出前对其进行拦截、修改、接管或转发,而上层调用者通常是无感知的。

WKWebView 的网络请求

下面基于 WebKit-7622.1.16 版本源码,从架构层面做一个初步分析。

WKWebView 采用的是多进程架构,核心可以分成以下几类进程:

进程种类 职责 数量
UIProcess 应用逻辑、API 1 个
WebProcess 渲染、JS 执行 视具体场景而定
NetworkProcess 所有网络请求 1 个
GPUProcess 图形渲染(可选) 1 个

通常情况下,一个网页的加载流程大致如下(这里先不考虑 GPUProcess):

  1. UIProcess:发起“加载 https://example.com”这一动作
  2. WebProcess:准备渲染环境
  3. NetworkProcess:发起 HTTP 请求
  4. 接收响应数据
  5. WebProcess:解析 HTML → 构建 DOM → 布局 → 绘制
  6. UIProcess:展示最终渲染结果

从这里可以看出,在 WKWebView 内部,网页相关的网络请求并不是简单地继续沿用 App 自己进程内的那套处理路径,而是由独立的 NetworkProcess 负责,进程之间通过 IPC 传递数据。

这里要特别区分一种情况。假设你在 App 里主动调用:

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

这个调用动作本身发生在 App 一侧,但这并不意味着后续网页加载过程就等同于 App 进程中的普通网络请求。对于 WKWebView 而言,真正的网页资源加载依然属于 WebKit 多进程体系的一部分。

尤其是在页面已经加载完成后,页面内部继续发起的资源请求、XHR、fetch 等行为,主要发生在 WebProcessNetworkProcess 的协作中,而不是走开发者熟悉的那套 App 侧可控路径。

也正因为如此,开发者常常会发现:即使能观察到某些请求,也拿不到完整的 POST body。在 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);
}

这段代码表达得非常直接:如果请求里包含 HTTPBodyHTTPBodyStream,就先创建一个可变副本,然后把这两个字段清空。也就是说,后续跨进程传递的数据,主要只保留了 URLheadermethod 等元信息。

之所以这样做,本质上和 WebKit 的多进程设计有关。

在多进程之间,数据传递需要经过“序列化 → 拷贝 → 反序列化”这一整套过程。请求体一旦较大,跨进程传输的成本就会明显放大。

同时,HTTPBodyStream 本身还是一种实时性很强的数据结构。它通常是一次性的、不可随意重复消费的输入流,跨进程复制之后也很难保证读取语义的一致性。

除此之外,把真正发送网络请求的位置固定在 NetworkProcess 中,也符合 WebKit 的安全与架构隔离目标:网络权限、缓存、证书校验以及其他策略,都可以在一个更集中、更可控的边界内统一处理。

结论

NSURLProtocol 主要作用于 App 可见的 URL Loading 体系,而 WKWebView 的网页加载则建立在 WebKit 的多进程架构之上。对于页面内部发起的网络请求来说,真正负责请求发送的是 NetworkProcess,而不是开发者平时直接操作的 App 进程网络栈。

更进一步地说,即使请求需要在 WebKit 内部多个进程之间传递,出于性能和实现约束,HTTPBody / HTTPBodyStream 也不会被完整地跨 IPC 传递。

因此,“NSURLProtocol 为什么拿不到 WKWebView 中某些请求的 POST body”这个问题,最终指向的并不是某个单点 API 的缺失,而是 WKWebView 背后的多进程网络架构本身。