iOS 端容器之 WKWebView 那些事

2021年09月15日 阅读数:5
这篇文章主要向大家介绍iOS 端容器之 WKWebView 那些事,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

简介: 本文主要是关于在端容器设计开发过程当中,WKWebView 使用上遇到的一些问题和解决办法。html

微信图片_20210721160150.jpg

做者 | 驽良

来源 | 阿里技术公众号前端

一  背景

熟悉 iOS\macOS Hybrid 混合开发的同窗应该都有体会,WKWebView 虽然是苹果做为替代 UIWebView\WebView 而推出的"新"组件,但大部分开发者对它实在“爱不起来”。毕竟对于国内大部分应用开发者来讲,在实际使用中 WKWebView 所谓的“优点”未必能体现出来,但带来的“坑”却都着实都不浅。c++

目前社区或线上可查找的 WKWebView 相关资料,大多比较陈旧且人云亦云、复制粘贴类的居多。少部分真实实践和探索的开发者,或许也因时间或精力的缘由,对问题和解决方案未能作详细的阐述。致使目前线上 WKWebView 相关的资料数量很多、但质量不高;且有很多文章存在对问题的背景解释不清,解决方案缺少有效验证等问题。web

我从事端容器领域开发多年,曾在生产环境方案设计上与 WKWebView "对抗"屡次。目前混合开发已是现代 App 标配,一方面是对这么长时间用法经验上的总结,另一方面也但愿可以为还在抗争中的同窗提供一些新视角或者解决思路,故准备结合 WebKit 部分源码,将本身对这个组件的理解以及部分问题解决方案整理分享一下。本文尝试说明 3 件事情:数组

  • WKWebView 使用中的典型问题有哪些
  • 为何会出现这些问题
  • 这些问题的解决办法有哪些

二 基础回顾

iOS 端网络设计和 WKWebView 设计特色咱们能够经过官方资料来查阅。但为了后面更好的说明问题,下面咱们重点回顾下与文章后续内容相关的两个基本知识点:缓存

  • iOS 端网络设计与 Cookie 管理
  • WKWebView 多进程模型

1  iOS 网络设计与 Cookie 管理

Cookie 管理是作混合开发过程当中常常会涉及到的部分,在应用开发中咱们知道能够经过 NSHTTPCookie 和NSHTTPCookieStorage 来管理应用的 Cookie。但在系统层面 Cookie 是如何管理的、如何与网络层各模块进行联动,这对咱们后面分析WKWebView 中的 Cookie 问题有着相当重要的联系。安全

根据官方资料,咱们可知 iOS 平台下网络相关模块大概关系以下:微信

微信图片_20210721160200.jpg

从上至下模块依次为:
  • WebKit:应用层,客户端 App 以及 WKWebView 处于这一层。
  • NSURL:能够理解为对底层 CF 接口的封装扩展层,NSURLConnection、NSURLSession 等处于这一层。
  • CFNetwork:iOS 网络模块核心实现层,是网络层设计中最重要的部分。负责网络协议组装发送接收等主要工做,与 CoreFoundation 框架关系紧密。
  • BSD socket:基于底层硬件接口的 socket 服务。

CFNetwork 是整个网络系统的核心模块,负责组装请求、处理响应等:cookie

微信图片_20210721160204.jpg

核心内容包含:
  • CFURLRequest:包括 URL/header/body 这些请求的信息。CFURLRequest 会进一步转换成 CFHTTPMessage。
  • CFHTTPMessage:主要是 HTTP 协议的定义和转换,把每个请求 request 转换成标准的 HTTP 格式的文本。
  • CFURLConnection:主要是处理请求任务。包括 pthread 线程、CFRunloop、请求队列的管理等等。提供了start、cancel 等等操做的 API。
  • CFHost:负责 DNS,有 CFHostStartInfoResolution 等函数,基于 dns_async_start 和 getaddrinfo_async_start 等方法。
  • CFURLCache/CFURLCredential/CFHTTPCookie:处理 缓存/证书/cookie 相关的逻辑,都有对应的NS类。

从上面分析可知关键信息:iOS Cookie 管理相关模块处于 CFNetwork 这一层中。即对于请求 Response 中的 "set-cookie" 字段,在 CFNetwork 中被消费和处理。网络

2  WKWebView 多进程模型

经过官方资料,咱们知道 WKWebView 相比 UIWebView 很大的一个变化是"多进程模型":

WKWebView 在运行时,核心模块运行在独立进程中,与 App 进程独立。

WKWebView 使各类问题的缘由,有很多和多进程运行模式有很大的关系。

多进程模型详解

但具体是什么样形态的多进程?咱们经过一张简图来讲明下:

微信图片_20210721160209.jpg

  • WKWebView(WebKit) 包含 3 种进程:UI Process, Networking Process, WebContent Process。

 

  • UI Process:即 App 进程,WKWebView(WebKit) 中部分模块运行在此进程,会负责启动其它进程。
  • Networking Process:即网络模块进程,主要负责 WKWebView 中网络请求相关功能;此进程 App 中只会有启动一次,多个 WKWebView 间共享。
  • WebContent Process:即 Web 模块进程,主要负责 WebCore, JSCore 相关模块的运行,是 WKWebView 的核心进程。此进程在 App 中会启动屡次,每一个 WKWebView 会有本身独立的 WebContent 进程。
  • 各个进程之间经过 CoreIPC 进程通讯。

总的来讲:在一个客户端 App 中,多个 WKWebView 使用中会共享一个 UI 进程(与 App 进程共享)、共享一个 Networking 进程、每一个 WKWebView 实例独享一个 WebContent 进程。

示例:

微信图片_20210721160213.jpg

此处关于 WebContent Process 和 Networking Process 的启动规则,官方文档并未解释特别清楚,且由于版本迭代等缘由,文档与目前最新规则也略有出入。为避免混淆与歧义,下面结合 WebKit 源码稍做分析。

WebContent 进程启动规则

根据官方文档描述:

A WKProcessPool object represents a single process that WebKit uses to manage web content. To provide a more secure and stable experience, WebKit renders the content of web views in separate processes, rather than in your app’s process space. By default, WebKit gives each web view its own process space until it reaches an implementation-defined process limit. After that, web views with the same WKProcessPool object share the same web content process.

规则是优先使用建立新进程,当进程上线超过某阈值以后则会共享,在 WebKit 内部由 maximumProcessCount 控制。可是此规则只对 iOS13 以前的系统生效,iOS13 以后的系统,WKWebView 每次建立实例都会启动一个新的 WebContent Porcess。相关实现以下。

iOS13 前:

微信图片_20210721160217.jpg

微信图片_20210721160220.jpg

iOS 13 及之后:

微信图片_20210721160224.jpg

Networking 进程启动规则

Networking 规则相对简单,确保在 App 生命周期内启动一例(Crash 以后会从新建立)。相关代码:

微信图片_20210721160249.jpg

三  主要问题与解决方案

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):

微信图片_20210721160258.jpg

故须要对携带 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 中对图片加载的流程来简单说明下:

微信图片_20210721160303.jpg

问题即出如今上述 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 ):188358 – WKURLSchemeHandler crashes when sent errors with sync XHR

 

Bug2: WKURLSchemeHandler crashes when sent errors with sync XHR(2019-06-20 01:20):199063 – WebURLSchemeHandlerProxy::loadSynchronously crash with sync request

处理方案:对于 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 的延迟。

WKWebView Cookie 问题

基于上节的 iOS Cookie 管理、结合多进程模型,咱们大概能够推断 App 与 WKWebView Cookie 管理模型,见以下简图:

微信图片_20210721160309.jpg

注意:WKHTTPCookieStore 为示意,画到了 Networking 进程,实际状况中此模块分散在 WebContent、Networking 以及 UI Process 中,且各进程中的部分经过 IPC 桥接。

根据上图能够引导出 WKWebView Cookie 相关的 2 个核心点:

1)WKWebView Cookie 问题具体是什么

  • 对于 "Session Cookie":App 进程与 WKWebView 进程(WebContent + Networking)之间 彻底隔离。
  • 对于 "持久化 Cookie":App 进程与 WKWebView 进程(WebContent + Networking)之间 同步存在时差。

2)形成 WKWebView Cookie 问题的根本缘由

  • App 进程 与 Networking 双进程的设计。

核心目标

在了解 WKWebView 问题以及对应的根本缘由以后,如何来处理此问题相对也清晰了:根据是否采用代理了 WKWebView 的网络请求,咱们须要不一样的处理策略。

  • 场景 1 - 未代理 WKWebView 网络请求:Cookie 彻底由 Networking 进程管理,WKWebView 内可自闭环。大部分状况下 App 进程也无需感知,若是确实须要感知,能够根据业务场景选择 JS 桥接、强制持久化等方案。
  • 场景 2 - 已代理 WKWebView 网络请求:Cookie 大部分是由 App 进程来管理,此时应该采用何种同步策略。

因为场景 1 中咱们并未在生产环境中采用,故本文不打算作冒然分析。下面主要聚焦于场景 2 来作进一部分分析。在场景 2 下咱们的核心目标:

  • 对于 App 进程中产生的 Cookie,可以及时同步到 Networking 进程:主要解决 Reponse 中存在 "Set-Cookie" 状况下,JS 端如何及时读取相关 Cookie 的问题。
  • 对于 WebContent 中由 JS 产生的 Cookie,能及时同步到 App 进程:主要解决在 JS 端产生 Cookie 以后,咱们如何保证在后续代理的网络请求中可被正常携带的问题。

同步手段

在确认方案以前,咱们首先要搞清楚一个问题:客户端侧 Cookie 来源有哪些?

对于 App 进程,Cookie 来源有两个:

  • 经过 NSHTTPCookieStorage 写入的。
  • 在网络请求 Response Header 中经过 "Set-Cookie" 写入的。

对于 WebContent 进程,主要是 JS 经过 document.cookie 写入的(网络代理以后 Set-Cookie 不会在 WKWebView 进程中生效)。

其次,咱们要确承认用作同步的手段有哪些:

对于 iOS 11 以后的系统,苹果已经为咱们提供了 WKHTTPCookieStore 对象用来读写、监听 WKWebView 对应的 Cookie,能够直接使用。

对于 iOS 11 以前的系统,须要区分处理一下。

从 App 进程同步到 Networking 进程,简单流程以下:

  • 第1步,须要把 Session Cookie 持久化,临时保存(注意须要标识,以供恢复)。
  • 第2步,调用 NSHTTPCookieStorage 内部接口 _saveCookies  触发强制同步。
  • 第3步,恢复临时保存的 Session Cookie,避免污染。

因为 Networking 进程不会产生 Cookie,因此咱们下面要作的是从 WebContent 进程同步 Cookie:处理策略即在 JS 侧重写 document.cookie 方法,在 JS 修改 cookies 时,经过 bridge 将 cookie 传递到 App 进程。

处理方案

在理清楚问题、目标和可用手段以后,咱们便可总结出 WKWebView Cookie 相关问题的处理方案:

  • 对于 iOS 11 及以后的系统,咱们能够经过 HOOK NSHTTPCookieStorage 中读写 Cookie 的接口,而且监听网络请求中 "Set-Cookie" 关键字,在 App 进程 Cookie 发生变化时同步到 WKWebView;同时经过 WKHTTPCookieStore 提供 cookiesDidChangeInCookieStore 能力来监听 WKWebView 中 Cookie 的变化。
  • 对于 iOS 11 以前的系统,处理策略相似。可是咱们须要过 NSHTTPCookieStorage 接口来作强制同步,而且须要注意恢复 Cookie 的 SessionOnly 属性;同时须要经过在 JS 侧重写 document.cookie 的方式,来感知 WKWebView 中 Cookie 的变化。

特别注意:

采用 iOS 11 以后方案处理时必定要注意,对 WKHTTPCookieStore 的操做会 涉及到 IPC 通讯,若是通讯过于频繁、通讯数据量过大,会产生明显的性能问题。极端状况可能形成 IPC 模块异常,出现全部 WKWebView 都没法加载的状况。好比典型的场景,若是一个页面请求较多、每一个请求都带"Set-Cookie"、且业务上为了简单,每次把 App 进程的 Cookie 全量同步到 WKWebView,则 Cookie 过多时,有必定几率(暴力测试可复现)触发 IPC 异常,致使后续全部 WKWebView 实例都没法正常加载,只有 App 杀进程才可恢复。建议在同步 Cookie 时,尽可能按需同步变化的部分。

3  全面屏适配问题

全面屏适配问题相对不复杂,但由于 WKWebView 如 UIWebView 在表现上的不一样,致使容易形成一些困扰。

问题是 UIWebView 与 WKWebView 在对前端 viewport-fit 支持表现上略有差异:UIWebView 对 viewport-fit 支持度较好,表现基本与官方文档表述一致。可是 WKWebView 中存在一个潜规则,若是 Web 页面内 body 的高度,在没有超出 WKWebView 组件实际高度时,viewport-fit=cover 可能不生效。

处理办法是在页面中规避掉此类状况,如配置 body height 为 100vh (或其它相似方案)。

4  WebContent 进程崩溃问题

这是一个出现几率不高,可是缺少通用、有效解决办法的问题。咱们知道 WKWebView 多进程模式下,若是 WebContent 进程由于各类缘由出现 Crash,则 WKWebView 会经过 webViewWebContentProcessDidTerminate 回调告诉开发者,通常状况下咱们会经过 reload 方法从新加载页面。同时若是用户设备内存紧张,则可能出现系统主动 KILL WebContent 进程的状况。便可能出现 App 进程(前台)正常,可是 WebContent 崩溃、页面从新加载的状况。

绝大部分状况,进入此流程并不必定会对用户操做形成困扰。可是,若是此时形成内存紧张是由于前端触发业务致使,典型如表单中唤起相机上传图片,此流程对用户的影响多是致命的。即便咱们经过 WebView reload 使页面恢复,用户在执行的上传动做也会被打断,致使提交流程出现异常、影响用户的操做。且若是用户设备进入此状态,大部分状况下用户再次操做还会触发一样的流程。

这种状况下,用户没法及时感知到形成问题的根本缘由,绝大多数直观反应即为:“App 出现 bug 了!”故从用户角度来看,缺乏自动恢复、处理问题的办法。

目前此问题缺少有效、统一解决办法,一种解决思路是客户端与前端配置,针对核心、可能出现异常的流程,定向设计解决方案。经过端侧的能力来将数据持久化,在相似异常发生以后使用持久化数据恢复现场,尽可能在用户无感的状况下保证用户操做流程正常。

四  总结

以上即是咱们在端容器设计开发过程当中,WKWebView 使用上遇到的一些典型问题和对应的解决办法。总的来讲,目前形成这么不协调的状态,大部分是由于系统平台未能充分考虑开发者诉求、组件设计对历史业务兼容性不佳致使的。固然,如今这种状态必然也不是一种合理状态,将来不管是系统平台方、仍是业务方、或者是开发者,当矛盾没法协调时总有一方要进行妥协。在这个时间点来临以前,但愿上面总结内容,可以为受此类问题困扰的同窗提供一些帮助。

原文连接
本文为阿里云原创内容,未经容许不得转载。