Networking 进程启动规则
Networking 规则相对简单,确保在 App 生命周期内启动一例(Crash 之后会重新创建)。相关代码:
WKWebView 在生产环境使用中,除去相对简单的使用和适配问题外,容易对开发者和前端同学造成困扰的问题有 4 个:
- 请求代理问题
- Cookie 管理问题
- 全面屏适配问题
- WebContent 进程崩溃问题
下面分别对这 4 例问题产生的原因、可尝试的解决方案以及不同方案下引入的问题做一下说明。
1 请求代理问题
这一点应该是阻碍 WKWebView 铺开的首要问题。问题背景也相对简单,并非有什么技术实现上的难度,而是苹果官方不希望 WKWebView 请求被应用拦截,美其名曰"为了安全"。但在实际使用场景中,我们又需要对 WebView 的请求进行代理以满足业务和性能诉求,典型场景如:离线包、流量监控等。
官方不支持、业务上又有使用场景,我们只能尝试通过"黑魔法"来解决。目前适用面比较多的解决方案有两个:
- 通过 [WKBrowsingContextController registerSchemeForCustomProtocol:] 来注册代理,为方便简称为代理方案1。
- 通过 [WKWebViewConfiguration setURLSchemeHandler:forURLScheme:] 来注册,为方便简称为代理方案 2。
目前两种解决方案的实现方法都有较为丰富的资料和说明,在此不在赘述。
虽然这两种方案某种程度上可以"部分解决问题",但带来的副作用相对也不少,在生产环境中如何取舍还需具体开发同学来取舍。下面分别通过 "代理方案1" 和 "代理方案2" 代指来简单说明下,可以供大家选型时做一个参考。
代理方案 1
此为最早出现的 WKWebView 请求代理方案,可满足 iOS 9 及以后应用使用(目前最新为 iOS 14)。根据之前调研分析,业界大部分有代理需求的 App 采用此方案或变种方案。
1)方案思路
通过 WKBrowsingContextController 将 http(s) 注册到 Networking 的 m_registeredSchemes 数组中。对于数组中的 Scheme,WebKit 在发起请求时会通过 WKCustomProtocol 将请求发送到 App 所在的进程,并由 App 进程来执行发送。
由于从 Networking 进程将数据发送到 App 进程时,WebKit 内有意剥离了 Body 部分(见 WebCoreArgumentCodersMac.mm):
故需要对携带 body 的请求做一些特殊处理。处理方案是通过在 WKWebView 内注入脚本,重写掉 WebView 内请求发送相关方法。在请求发送之前将 body 部分序列化之后通过 bridge 传递到 App 进程暂存。
App 进程代理 WKWebView 请求时,根据规则按需拼接缓存的 body,完成之后再进行发送动作。
2)方案弊端
此方案虽然适用面较广,但是弊端也很明显。主要有两方面:
(1)问题一:无法定向处理、只能一刀切即如果 App 采用此方案,对其所有 WKWebView 实例的发送的请求都需要代理。如果有 WKWebView 实例中并未注入脚本或者执行代理,则可能导致请求无法发送、发送缺少 body 等问题,常见于一些集成的二方库、三方库中的 WKWebView 实例。
(2)问题二:重写脚本完备性很难保障由于需要在 JS 层重写请求发送逻辑,比如 form 表单提交、AJAX、Fetch 等接口,重写接口的质量直接决定方案的完备度。且 WKWebView 原设计有不少能力在 c 层面实现,仅在 JS 重写无法保证对齐。目前已知的问题有:
- 对于同步请求,此方案目前无法支持。
- 对于流式请求,比如上传场景,目前支持度较差。只能在 JS 侧全量读取之后再进行发送。
- 无法处理 Fetch API Stream 返回值。
- 当使用 Form 表单提交内容包含大块数据时,可能出现丢失、Crash 等情况。
代理方案 2
此方案是基于苹果在 iOS 11 上开放的 [WKWebViewConfiguration setURLSchemeHandler:forURLScheme:] 接口做"扩展"来实现。对于 iOS 11.3 以后的设备,此方案具备较好实用性(WebKit 处理了部分 Body 传递问题)。
1)方案思路
- [WKWebViewConfiguration setURLSchemeHandler:forURLScheme:] 可以在 WKWebView 实例上注册自定义请求 Scheme。如果 WKWebView 发送的请求匹配注册 Scheme,则会代理到 UI 进程(App 进程) 执行发送动作。
- WKWebView 内部默认不支持注册 http(s) 等标准 Scheme,但是有"黑魔法"可绕过限制。
- 对于 AJAX 发送 BLOB 数据时,也会出现 body 丢失的情况,可以参考 代理方案1 中类似的方案来解决。
2)方案优势
代理方案 2 相对方案 1 两个巨大的优势在于:
- 不用一刀切,配置与 WKWebView 实例绑定:即可以定向处理我们需要处理的 WKWebView 实例,对于 三方库 中的对象,完全可以做到无影响,安全性大大提高。
- 不用重写所有发送请求:大部分情况下请求中的 body 是可以被携带到 App 进程,即我们只需定向处理部分异常即可,健壮性大大提升。
3)方案弊端
此方案除去有 iOS 11.3 的系统版本限制外,在具体运行中也有也有不少很难处理的问题,主要如下:
(1)问题一:多图分片下载情况下,WKWebView 内部存在处理时序存在 BUG
问题表现:在 WKWebView 中加载大图、且大图数据存在存在分片返回时,WKWebView 内部时序处理异常可能导致 图片无法展示、图片展示不完整等问题。具体可结合 WebKit 中对图片加载的流程来简单说明下:
问题即出现在上述 step1, step2, step3 的执行顺序上。在异常情况下,会偶现执行顺序为:step1 -> step3 -> step2, 且 step3 不再被触发(allDataReceived),进而导致图片最终的内容未渲染上屏。
解决方案:目前暂无有效的解决办法,通过配置 suppressesIncrementalRendering 配置为 YES 某种程度上可以缓解问题,但并无法根治且对体验略有影响。
(2)问题二:iOS 12及以下系统系统同步 AJAX 导致 Crash
问题表现:在 WKWebView 中如果出现 Web 页面发送 sync request,则可导致 WebContent 进程崩溃,WKWebView 回调 webViewWebContentProcessDidTerminate,进而导致页面白屏等问题。此问题可明确是 WebKit 内部的 BUG,且已有相关 Fix:
Bug1: WebURLSchemeHandlerProxy::loadSynchronously crash with sync request(2018-08-06 14:14 ):https://bugs.webkit.org/show_bug.cgi?id=188358
Bug2: WKURLSchemeHandler crashes when sent errors with sync XHR(2019-06-20 01:20):https://bugs.webkit.org/show_bug.cgi?id=199063
处理方案:对于 Bug1,处理相对比较简答,即在网络请求回调 error 之前优先回调部分空数据,规避掉问题;但是对于 Bug2,目前缺少有效的解决办法。
(3)问题三:301 请求下 SWAP 导致页面无法转场问题
问题表现:如果页面中存在使用 301 做重定向的情况,可能会出现重定向页面无法加载的情况,进而导致页面异常、白屏等问题。
处理方案:关闭 processSwapsOnNavigation,将置为 NO(内部属性)。
总的来说,虽然代理方案 2 相比代理方案 1 有很大的优势,但此方案因为版本限制等原因,目前使用量相比代理方案 1 略低。且与代理方案 1 相比,此方案的优势和劣势都很明显,如多图分片场景下可能导致图片无法展示的问题,目前未找到有效的解决办法。方案 2 是否可替代方案 1 在生产环境使用,还需使用同学自己斟酌。
2 Cookie 问题
根据官方文档及资料,我们可知 WKWebView 因为其"独立存储",导致 Cookie 和 Cache 与 App 不互通,进而有问题。但是这种表述较为模糊,且实际使用中 WKWebView 与 App 的 Cookie 并非完全隔离,这种模棱两可的表现很难让人搞清楚"通"或"不通"的边界在哪里。
下面首先根据自己对这块的理解,尝试说明下 WKWebView 使用 Cookie 问题到底是什么、以及背后的原因。由于苹果并未对全部代码开源,下有不少内容是自己的理解和推断,无法保证完全正确,仅介绍部分思路和判断,供大家在需要时参考。
Cookie 管理策略根据上节的背景介绍我们可知,iOS Cookie 相关内容,是在 CFNetwork 这一层由 CFHTTPCookie、CFHTTPCookieStorage 等来管理,是 CFNetwork 模块的一部分。且对于 Session Cookie 和 持久化 Cookie,系统有着不同的管理策略:
- Session Cookie:内存中保存,进程周期内生效。在 iOS 移动端,一个 App 进程 即对应于一个 Session,即 Session Cookie 可在进程内共享。
- 持久化 Cookie:这部分 Cookie 除保存内存以外,还会持久化到磁盘,可多次使用。本地文件存储在 沙箱文件夹/Library/Cookies/Cookies.binarycookies;需要特别注意的是:持久化 Cookie 并非在产生之后立即同步到 Cookies.binarycookies,根据经验会有一个 300ms ~ 3s 的延迟。
基于上节的 iOS Cookie 管理、结合多进程模型,我们大概可以推断 App 与 WKWebView Cookie 管理模型,见如下简图: