-
WKWebView不支持NSURLProtocol
前段时间总结过《WKWebView从入门到趟坑》,其中提到 NSURLProtocol 拦截支持和缓存的痛点。在 UIWebView 时代,按照下面的方式注册一个自定义的 NSURLProtocol 和 CustomURLCache 的子类,
// 注册url拦截
[NSURLProtocol registerClass:[CustomURLProtocol class]];
// 注册缓存加载
[NSURLCache setSharedURLCache:[CustomURLCache new]];
然后就可以在其实现中对 app 内所有的网络请求进行拦截,并加载本地的离线资源(很多 Hybird 框架都是基于此原理)。但在 WKWebView 中的请求却完全不遵从这一规则,网络上文章一般都解释说 WKWebView 的请求是在单独的进程里,所以不走 NSURLProtocol。
断点调用栈也显示 WKWebView 在加载 page 时候的确使用的是 WebKit 的 IPC 消息收发/分发机制 MessageQueue 进行进程之间的通信,但也发现它竟然调用到了
+ [NSURLProtocol canInitWithRequest:]
之后的流程就不在执行 NSURLProtocol 相关的方法了,既然调用了一个 canInitWithRequest,至少说明 WKWebView 还是引用了 NSURLProtocol 类或其子类,只是不知道什么原因没有实现其他调用(很多人都评论 WKWebView 是未开发完善的半成品)。
好在 WKWebView 的内核是基于开源的 WebKit,让好奇地开发者们能够翻看源码一探究竟,传送门:https://github.com/WebKit/webkit。在一个测试用例下(webkit/Tools/TestWebKitAPI/Tests/WebKit2Cocoa/AlwaysRevalidatedURLSchemes.mm),看到了熟悉的 WKWebView 加载流程代码,如下图:
注意红色标记处,正是 UIWebView 时代 NSURLProtocol 拦截方式,可见源码上 WKWebView 似乎是支持 NSURLProtocol 的,在 WebKit.framework 中并没有 WKBrowsingContextController 的身影,说明这是个未开放的类,它到底是个什么鬼呢?猜测它可能与 NSURLProtocol 有关。
-
WebKit 源码浅析
众所周知,WebKit 源码由三大部分组成:
-
WebCore:HTML 排版引擎核心,主要包含 Loader,Parser(DOM,Render),Layout,Paint等模块
-
WebKit:移植层,主要包括 GUI,File System,Thread,图片解码等与平台相关的模块
-
JavaScriptCore:JS 虚拟机,主要用于操作 DOM,解析执行 JavaScript 代码
其源码架构如下:
与 WebKit.framework 相关的源码脑图如下:
红色标记处即苹果并未开放出来的接口类,其中:
-
WKScrollView 负责 Page 的一些滑动,手势操作等。
-
WebContenView 负责 Page 的渲染。
-
WKBrowsingContextController 负责浏览器的交互处理,提供了一些通用的操作,如加载,前进,后退,刷新等。
-
WKBrowsingContextControllerHandel 提供可扩展的操作的抽象,插件化实现各种浏览器功能。
-
WebPageProxy 是 WebKit 的 Page 代理,实现 UI Process 层和 Web Process 层的接口。
-
WebProcessPool 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享数据。不过 WKProcessPool 实例在 app 杀进程重启后会被重置,无法实现本地化保存。
参考博文中提到的:
http://blog.csdn.net/horkychen/article/details/8589449
可以很清楚地了解 WKView(WKWebView 是其在 iOS 平台的一个实例)在 WebKit 中的初始化流程:
-
根据配置项 WKWebViewConfiguration 创建新 WKWebView,同时会初始化 WKScrollView 和 WKContentView;
-
WKContentView 从进程池 WKProcessPool 中分配 WebProcessProxy 和 WebPageProxy,同时根据当前 Page 初始化 WKBrowsingContextController,提供了大部分交互操作功能;
-
WKWebView 在独立于 App Process 进程之外的 Network Process 进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。
其中有两个方法是前面测试用例中见到的:
可见内部对全局的 WebProcessPool 进行了自定义 scheme 的注册和注销。继续查看 WebProcessPool 的源码:
发现其实 WKWebView 也会将 Cookie 存储于 NSHTTPCookieStorage,同样Cache 也会存储于 NSURLCache 中,只是 SandboxExtension 会把数据
encode 成一个 Message 通过 IPC 发送给 App Process 进行异步存储的,这就会存在时机问题,并不是业界普遍认为 WKWebView 拥有自己的私有存储。
在 globalURLSchemesWithCustomProtocolHandlers() 中获取的即是已注册的自定义 scheme。从前面的源码中可见这些已注册的 scheme 被当作配置参数 (NetworkProcessCreationParameters) 传递给 Network Process 进程,而在其初始化中
CustomProtocolManager 通过 WKCustomProtocol 负责拦截处理自定义的 scheme,同时把 Network Process 中发起的网络请求发送到 App Process 进程,交由 CustomProtocolManagerProxy 代理重新发起实际的网络请求,最后把响应信息交还给 WKCustomProtocol 进行处理。
总结一下流程如下:
-
WKBrowsingContextController 通过 registerSchemeForCustomProtocol 向 WebProcessPool 注册全局自定义 scheme
-
WebProcessPool 使用已注册的 scheme 初始化 Network Process 进程配置,同时设置 CustomProtocolManager,负责把网络请求通过 IPC 发送到 App Process 进程、也接收从 App Process 进程返回的网络响应 response
-
CustomProtocolManager 注册了 NSURLProtocol 的子类 WKCustomProtocol,负责拦截网络请求处理
-
CustomProtocolManagerProxy 中的 WKCustomProtocolLoader 使用 NSURLConnection 发送实际的网络请求,并将响应 response 返回给 CustomProtocolManager
至此,完成了一个完整的网络请求代理拦截处理流程。可见 WKWebView 源码是支持 NSURLProtocol 拦截的。
-
如何让 WKWebView 支持 NSURLProtocol
既然知道了源码逻辑,就可以仿照测试用例中的代码来实验上述结论,这个已经有大牛完成了 Demo 例子 https://github.com/yeatse/NSURLProtocol-WebKitSupport,这里就只贴出自己测试时候的关键代码。
实验结果证实 WKWebView 是支持 NSURLProtocol 拦截的,只是 WebKit.framework 还不完善。
额外提一下,越来越多的人开始适配 WKWebView,也趟了不少坑,总结了不少经验,其中腾讯 Bugly 公众号上总结的《WKWebView 那些坑》非常深入。也解决了本人遇到 post 请求 body 数据丢失问题的疑惑,受益匪浅。