WKWebView 的性能优化

作者: 這Er | 来源:发表于2019-03-21 15:46 被阅读1次

    WKWebView 的性能优化

    起因

    随着移动设备性能不断增强,web 页面的性能体验逐渐变得可以接受,又因为 web 开发模式的诸多好处(跨平台,动态更新,减体积,无限扩展),APP 客户端里出现越来越多内嵌 web 页面),很多 APP 把一些功能模块改成用 H5 实现。

    虽然说 H5 页面性能变好了,但如果没针对性地做一些优化,体验还是很糟糕的,主要两部分体验:

    1. 页面启动白屏时间:打开一个 H5 页面需要做一系列处理,会有一段白屏时间,体验糟糕。
    2. 响应流畅度:由于 webkit 的渲染机制,单线程,历史包袱等原因,页面刷新/交互的性能体验不如原生。

    由于以上原因,公司准备从第一点入手,做 webview 的优化项目(达到秒开 webview )。因为UIWebView在 iOS12 就被标记废弃了,所以决定先从WKWebView入手研究。

    思路

    webview 加载过程

    webview展示流程.png

    打开一个页面的过程有很多优化点,包括前端和客户端,常规的前端和后端的性能优化已有前辈们总结过最佳实践,主要的是:

    • 降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
    • 加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
    • 缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
    • 渲染:JS/CSS优化,加载顺序,服务端渲染模板直出。
    • 客户端:预请求web所需数据
      大家可以看出,主要是第三阶段之前,用户看到的页面一直处于白屏。首先要优化的就是这段时间。
      打开一个页面的过程有很多优化点,包括前端和客户端,常规的前端和后端的性能优化已有前辈们总结过最佳实践,主要的是:

    下面只讲客户端优化部分:

    减少第一阶段耗时

    1. 在使用前预先初始化好 webView,从而减小耗时。
    2. 在初始化的同时,通过 Native 来完成一些网络请求等过程,使得 webView 初始化不是完全的阻塞后续过程。
    3. webview 池,可以用两个或多个 webview 重复使用,而不是每次打开 H5 都新建 webview。

    减少第二阶段耗时

    1. 离线包
      1. 预先下载离线包,可以达到立即展示的效果。
      2. 离线包可以很方便地根据版本做增量更新。
      3. 离线包以压缩包的方式下发,同时会经过加密和校验,防止运营商和第三方对其劫持篡改。
    2. 数据缓存
      1. 第一次打开会有延迟,但是后续打开就会很快
      2. 可以自己控制缓存,方便管理
    3. 客户端代替请求
      1. 客户端可以在网络请求上做像 DNS 预解析/ IP 直连/长连接/并行请求等更细致的优化

    难点

    方案是通用的,不区分 UIWebView 和 WKWebView,但是目前很少有以 WKWebView 为目标的方案,那么以上技术方案在 WKWebView 中实现有什么难点呢?
    难点在 NSURLProtocol

    WKWebView 无法使用 NSURLProtocol 拦截 http 请求

    这个问题网上早有方案:
    [WKBrowsingContextController registerSchemeForCustomProtocol:@"schemes"];

    WKWebView 使用 NSURLProtocol 拦截后,HTTPBody的数据会丢失

    从网上克隆了 webkit 进行编译调试,尝试解决 Body 丢失的问题(体验到了啥叫大型项目的编译速度)


    image.png

    从图上重点标注的地方可以看到:

    1. WKWebView 的网络请求是在另外一个进程中操作的,然后如果 app 主进程需要拦截请求的话,通过 XPC 来进行两个进程间的通信。

    2. 苹果出于性能或其他考虑,会在给主进程的 URLProtocol 传输请求时将 HTTPBodyHTTPBodyStream 置为 nil 。
      源代码

    尝试解决方案:
    1. 使用 runtime 黑魔法,在其将 HTTPBody 置为 nil 之前,先保存下来?
      因为网络请求是在其他进程中操作,没有办法在主进程使用 runtime 进行拦截。也就是说在 app 中决定拦截 http 请求的那一刻起,拦截到的请求注定是没有 HTTPBody 的。
    2. 使用 任何方式进入到 Networking 进程做一些操作 ?
      尝试了 Mac 端的 XPC demo,XPC 的回调是在各自进程,是不能操作其他进程的。
    3. HTTPBody 置为 nil 之前,是否会有代码走到主进程,然后拿到 request 进行操作?
      抱歉,经过测试,在 HTTPBody 置为 nil 之前,主进程不会收到关于 request 的调用
    xpc 的 demo

    解决

    难到就没有任何方法解决了么?无意中看到一个特别有趣的想法又点燃了我的希望。

    既然 Networking 进程会将 HTTPBody 置为 nil ,那我要做的就是两点:
    1. 不让其置为 nil
    2. 或者在其置为 nil 之前,先将 HTTPBody 保存下来

    第一点:上面已经尝试失败;第二点:在 native 端也尝试失败,那在 H5 侧做保存操作呢?
    要拦截的是 H5 的请求,那说明 H5 侧肯定是知道请求参数的。

    尝试 H5 与 native 结合来解决 HTTPBody 丢失问题

    H5 发起发起请求有三种方式:
    1. Form
    2. XMLHttpRequest
    3. Fetch

    Fetch是在 iOS10 以后支持的,从通用场景看,只需要处理 FormXMLHttpRequest 发起的带 HTTPBody 的请求就可以.

    基本所有 H5 开发者,肯定知道 H5 里面也有黑魔法,就是原型:
    XMLHttpRequest 对应的是 XMLHttpRequest.prototype.send 方法
    Form 对应的是 HTMLFormElement.prototype.submit 方法

    我们对以上方法使用 WKUserScriptWKUserScriptInjectionTimeAtDocumentStart 时机做对应拦截。这样 H5 在发起请求前,先将 POST 的数据发送给 native 存储(WKScriptMessageHandler)。然后在 native 拦截到匹配到的请求,尝试接管,并重新设置 HTTPBody,而且由于拦截到的是 request ,只需要补齐HTTPBody,其他在 h5 中原本对 request 做的各种操作也是存在的,这样就能解决问题了

    这个方法提供了一种解决HTTPBody 丢失问题的可能,并且大部分 app,使用应该完全够用。本人已经按照上述方案实现,并接入到 app 中,在解决了一些细节问题后,将各个流程中的 H5 页面走了一遍,目前没有发现不支持的请求。


    感兴趣的可以自己下载编译 webkit
    XPCDemo


    参考:
    WebView性能、体验分析与优化
    移动 H5 首屏秒开优化方案探讨
    IMYWebLoader
    VasSonic

    相关文章

      网友评论

        本文标题:WKWebView 的性能优化

        本文链接:https://www.haomeiwen.com/subject/bnlgmqtx.html