作者:李天星,负责牛客APP(iOS)开发。

背景

在介绍优化过程之前,我们先来看一下牛客App的一个技术历程,以iOS为例(Android少许地方会有些差异)。

  • 第一阶段:Hybrid为大部分业务承载主体,Native更多是服务提供方;
  • 第二阶段:开始接入新的跨端技术--Flutter;
  • 第三阶段:制定了《移动端需求页面技术选型规则》,技术栈趋于稳定,为Flutter、Native以及Hybrid,需求迭代过程中综合开发效率、业务特性等多方面因素来决定适用技术,在过去一段的改版过程中,部分业务技术栈由Hybrid迁移到Native。
  • Native:是指以Android、iOS各自系统提供的开发工具、语言以及UI框架进行开发,其中Android侧开发语言为java、kotlin,iOS侧开发语言为Objective-C、Swift;
  • Flutter:近几年Google推出的跨端UI框架,可在Web、iOS、Android等多端使用,使用开发语言为Dart,性能在跨端框架中较好。
  • Hybrid:传统的跨端技术,是指用前端框架、技术开发页面,然后Android、iOS两端使用各自平台提供的Native原生控件WebView对其进行加载,对于其无法获得的能力(获取沙盒文件等)可利用Js与Native通信的方式从Native侧得到补充,虽开发效率及动态性较好,但性能一直被人诟病。

来看这次需要优化的帖子终端页,其页面主体为帖子内容以及评论、回复等,其形式都是为富文本,这种业务形态结合之前制定的选型原则,更适合继续使用Hybird形式,不适合为了提高性能,将其技术栈迁移到Native。

接下来我们就将本次优化问题聚焦到如何提高Hybrid的加载速度上。

原理&优化思路

在优化之间我们先得搞明白移动端WebView打开一个网页的步骤及原理,了解其过程中花费的时间分配的阶段,这样我们才能对其进行优化。 打开流程 一般来说, WebView渲染需要经过下面的几个步骤:

  1. 第一阶段:无反馈
    • WebView初始化
  2. 第二阶段:白屏
    • 获取并解析HTML文件
    • 获取并加载JavaScript、CSS等文件
    • 构造DOM结构
  3. 第三阶段:页面静态框架出现,没有数据,处于loading状态
    • 获取及接收数据
    • 渲染(加载图片、文本内容等)

根据上面描述,我们可以知道一个页面的加载过程,那如何缩短各个阶段的时间,就成为优化WebView性能的关键,并且通过上述加载过程,我们可以知道Hybrid的优化不是Native、前端某一侧可以完全独立完成的,而是需要两侧合作,共同完成。接下来我们结合牛客Hybrid现状分析一下各阶段的优化手段。

App现状&优化

WebView初始化

当App首次打开时,默认是并不初始化浏览器内核的;只有当创建WebView实例的时候,才会创建WebView的基础框架(可以理解这个动作是打开了一个浏览器)。并且这个过程是阻塞的,WebView未初始完成,后面的操作都无法进行。

根据美团的测试,这个阶段的耗时大约为:

WebView初始化耗时

这块是17年的测试数据,并且测试设备是模拟器,数据与目前主流设备相比会有一定的差距,理论上,目前的主流设备会更快一些。

我们可以看到这个阶段的耗时还是相当大的,那怎么进行优化呢?

提前WebView的初始化时机。在应用启动之后,初始化WebView缓存池,并预加载一定数量的WebView(如果是单一类型也可直接定义全局WebView),这样等到用户访问页面时,就不需消耗这个时间了。

当然这个过程也带来了额外的内存消耗,本质其实就是以空间换时间。

这一阶段根据业务特性也可进行其他的操作,如网络请求等。

具体到本次优化,App启动后,初始化一个帖子类型的WebView,并加入到缓存池中,这里之所以采用缓存池的方式而且还分类型,主要是为了后续同类型优化考虑,提前打好基础。

获取HTML等资源文件

获取HTML等资源文件以及加载HTML等资源文件这两个阶段并不是先全获取再去加载,而是在加载过程中用到相应的资源再去获取继而加载,这里为了更好的描述优化点,统一分成这两个阶段。

传统的WebView加载网页是开发者提供一个URL,WebView内部便去服务器端请求下载该URL对应的HTML页面,在HTML加载过程中,再去服务器请求下载相关的JS、CSS等文件。 ​

如果采用传统方式,那在这个阶段的耗时大部分来自于网络传输。对该阶段的优化目前主要会有两种大方向:

这里如果从网络请求、前端等方向出发,也会有很多优化点,比如CDN、资源压缩等,这里因为方案方向原因,就不展开描述了。

  • 离线包:将相关的资源文件直接存放在App安装包内或者沙盒磁盘中,这样获取相关资源文件就从网络请求转为从磁盘请求,获取速度大大提升。
  • 对第一次加载过的CSS、JS等资源文件进行持久化存储,第二次请求时直接从磁盘读取资源即可;

目前更主流的是离线包方案,配合离线包平台可进行热更新。

现阶段牛客App已经是离线包方案,资源直接来自于沙盒磁盘,在这个阶段的耗时已经很低,所以目前无需再在这一阶段进行优化。

加载HTML等资源文件

这个阶段的加载时间其实跟设备的性能关系比较密切,设备性能越低,该阶段耗时占比其实上会越高,而且这部分优化方向也可以分成两个方向去进行。

前端侧

这个方向就属于前端方向的优化,这里不具体展开,一些优化措施具体可见雅虎前端优化规则(Yslow)-Best Practices for Speeding Up Your Web Site

这个方向上的优化实际更多是一种通用的优化,不依赖或很少依赖于移动端侧与原生侧的交互。

原生侧

既然WebView充当的是浏览器的角色,当相关资源加载环境完毕之后,这个WebView实例本身便拥有了这个页面所需的环境,那只要对这个实例不进行销毁,相应的环境就会一直存在。

按照这个思路,结合帖子终端页这个具体业务来看,我们可以将帖子终端页当做一个模板页面,提前使用WebView进行加载,这样等到我们真正去打开页面使用这个WebView的时候,此时的WebView已经有了相关的环境,便不会再在加载JS等资源文件这个阶段耗时,我们剩余要做的只需要通知前端侧去获取数据即可。

此次优化我们主要在原生侧方向上进行优化,具体措施为 Hybrid 添加了两个方法供原生调用,

  • setData方法:参数支持传id、data两种形式,如果传id,Hyrbid侧内部执行网络请求拿到帖子数据,如果传data,Hybrid侧直接将数据填充到页面上;
  • clearData方法:将页面数据清除;

这里为什么要让setData支持两种形式,可见《获取数据》章节。

获取数据

请求接口获取数据这个步骤是每个业务基本上都少不了的,除了针对业务去减少字段这类优化措施之外,目前牛客App使用到的优化手段还包括:

  • Native端接管Hybrid页面发出的网络请求,这样可以集中控制App发出的网络请求,从优化方面上讲还可以共享DNS缓存等;
  • Native接管Hybrid页面中图片的加载,这样可以共享Native的图片缓存机制,包含内存缓存以及磁盘缓存等;
  • ...

回到帖子这个具体业务上来,在上述章节我们提到我们可以让Native直接调用JS的setData方法的形式去让页面获取或者直接填充数据。在我们最初制定方案时,我们就确定要支持这两种形式,具体原因是

  • id这种形式可以让帖子终端页没有对外部数据的依赖,同时可以享受到Webview的环境缓存,但因为其内部还需要网络请求数据,可能会达不到预期的秒开效果;
  • data这种形式可以抹平获取数据这个阶段的耗时,但是缺点是需要外部为其提供数据,依赖性较强;

我们可以根据业务外部环境特点选择性的使用这两种方式,其实对于首页列表进入帖子终端页这个需求来看,外部有能力提供数据,并且对加载速度有明确要求,我们选用的是第二种方式。

同时Hybrid内部在这个阶段也做了减少多余网络请求的优化。

第一种方式实际测试中发现也确实达不到秒开效果。

页面加载流程

我们再来整体梳理一下帖子终端页面的优化后的加载流程。

  1. App启动后,会初始化一个帖子类型的WebView,并开始加载帖子终端模板页面,并加入到缓存池中,加载完成模板完成后,为WebView打上一个标记进行标识;

    这里分帖子类型主要为了后续其他业务进行类似优化做一些准备;

  2. 用户在首页查看列表时,将帖子数据记录;
  3. 用户点击列表打开帖子终端页面,优先从缓存中取出打完标记的WebView,并调用setData方法将数据传输到Hybrid;
  4. 用户离开帖子终端页面,调用clearData方法清空数据。

    清空数据原因是为了防止进入新的帖子终端页面出现先显示上次加载的帖子数据再出现本次帖子数据的现象。

遇到的问题

  • 调试问题
    • 帖子终端页Hybrid代码中有一个小坑,一个叫做 refresh 的方法名在第一次加载后被改写成一个类名,造成调试花了一些时间。
  • 两侧数据同步问题:
    • 帖子终端页所需数据由前一列表页面传递过去,会造成用户在终端页面进行操作改变一些数据后,列表页与终端页数据不同步,造成再次进入时使用的还是列表上历史数据的问题。

      解决办法是当终端页进行对数据改变的操作是,将数据同步到列表上,此措施也同时解决了过去列表上点赞等状态与最新数据不一致的历史问题。

    • 原生侧从接口拿到的数据不满足帖子终端页所需,缺少部分字段,还有部分字段两侧名称不一致;
  • 阅读量问题:
    • 终端页面因为不需要再调用接口获取帖子数据,会造成无法通知后台进行阅读量的增加;

      解决方式为:前端单独调用一个增加阅读量的接口;

关于两端数据同步问题后续类似业务优化时可对方案进行改进:Native侧只传一些核心数据到终端页,如内容、标题、作者等,进入终端页之后,可以再进行一次网络请求去填充剩余数据。

实际效果

在优化之前,我们进行了数据埋点,其中Android初始化WebView到JS加载完成这段时间平均下来为350ms,iOS平均下来为600ms。

但之前iOS开始加载的时机相对靠后,造成加载时间多出600ms的耗时。所以iOS在这次优化之前就进行了一次优化,优化手段就是将加载时机提前,抹平多出的600ms耗时。

至于优化的效果,我们直接看吧。测试机型为iPhone 8,测试内容均为同一篇帖子;

优化前 第一次优化 第二次优化,传id形式 第二次优化,传data形式
优化前.gif 第一次优化.gif 第二次优化,传id形式.gif 第二次优化,传data形式.gif

参考文档