服务器 频道

高兼容低成本,开箱即用的首页性能优化方式被我们找到了

  2020年初,小红书首页 UI 的复杂度显著提升,在优化布局 xml 和使用一些 stub 方式的同时,我们也在寻找一些成本更低、性能更好的方式。

  X2C 是当时业界熟知的一种优化方式,其原理是编译期将 xml 翻译成代码,可以有效避免反射以及读取资源文件的损耗。由于小红书 APP 中存在着很多自定义 View 的场景,X2C 同时也会带来较高的维护成本。

  经过对 LayoutInflater 耗时的深入分析,我们找到了可以兼容各种 View 场景的 APT 方案。这一方案既避免了反射所带来的损耗,也不会增加额外的维护成本,成为了一个开箱即用的工具。  

  我们的探索灵感来自于 ViewCompiler 。作为 Google 的一个实验性工具,ViewCompiler 可以手动地将 xml 布局转化为 java 文件或者 dex 文件,但它并不支持 merge 和 include 标签。  

  ViewCompiler 在 Android Q(Android 10)的时候被引入,目前来说也还是一个实验性质的工具,因此我们平时并没有办法使用它。下图为Android S(Android 12)中的源码,大家可以看到这项功能未被开启。  

  其原理也很简单,先生成一个模板代码片段,然后再生成遍历 xml 的逻辑代码。  

  这样做的主要好处是可以节省掉反射带来的时间消耗,官方在 AppCompatViewInflater 中已经处理了原生 View 的创建,通过直接匹配名称 new 对象,避免了使用反射造成的性能开销。  

  

  在日常使用中,反射性能开销主要集中在自定义 View 这部分,我们的 App 本身就是一个自定义 View 非常多的场景,所以天然适合这种 VIewCompiler 的这种方式。同时,因为在遍历 xml 的时候,每一个 attrs 都会遍历到,所以它在维护性上也有着巨大的优势,我们不需要对自定义的 attrs 做任何处理。

  基于对 X2C 和 ViewCompiler 的源码和生成代码的阅读,我们决定做一个可以生成 Kotlin 代码,同时也解决 ViewCompiler 不支持的 include 和 merge 两个标签。我们用到的工具比较常规,有 kapt 和 kotlinpoet,整体的思路是通过 Resources.getLayout 取到 XmlResourceParser,然后通过 parser 的不断 next 来遍历每一个 xml 中的 tag,生成的代码示意如下:  

  在遇到 merge 和 include 时,我们需要特殊处理递归调用的逻辑,以便可以将父子布局连在一起。

  用这种新的方式替换掉首页中一些布局的实现后,我们发现,线上首页部分 p90 的布局时间减少了 200ms+,时长、CES、留存等指标均得到了显著提升。  

  LayoutInflater 的工作过程

  LayoutInflater 的工作过程可以用下图来简易表示:  

  本文所阐述的方案就是利用 apt 在编译期间生成代码,在便利解析 layout 文件之后,我们使用生成的代码直接创建实例,其效率与命中 AppCompat 基础组件逻辑之后的效率在理论上是一致的。

  AppCompat 基础组件可以查看 AppCompatViewInflater.java 源码(上文也有部分展示),其中包括了诸如 TextView、Button 等十几个常用的基础组件。  

  就一个具体的布局而言,能够通过 Layout2Code 的使用得以提升的性能只有除了基础组件之外的其他组件,尤其是当布局使用了大量自定义组件时,效果尤为明显。

  这也给了我们另一个提醒。如在 xml 中写 TextView / TextViewCompat,在 AppCmpatViewInflater 的作用下最终创建的实例都是 TextViewCompat。但在不使用 Layout2Code 或类 X2C 方案时,它们的效率是不同的,前者命中上图的直接创建逻辑,而后者则会通过反射创建。  

  X2C的不足

  X2C 除了做了以上优化,还将 layout 文件的读取和解析也一并移到了编译阶段,以此来降低 IO 开销。但编译期解析 xml 最大的困难在于我们需要逐条翻译 View 的属性,原因是编译期间并没有 SDK 的依赖,因此无法生成 AtrributeSet 对象直接供以 View 的构造器消费。

  这样一来,需要人工维护翻译规则,将一条条 xml 属性转换成设置 View 属性的代码,这带来了几个问题:

  1. 生成的代码量指数级增加

  2. 需要极高的维护成本来支持自定义 View 的属性

  3. 某些 xml 属性并没有相对应的方法或不是一一对应的。

  总而言之,在此基础上要维持健壮完备的功能是非常困难的。而我们所探索的 Layout2Code 的新方案与之相比,兼容性和维护成本都有着巨大优势,唯一需要权衡考虑的就是运行时读取 layout 文件的优化空间有多少,是否值得这样的投入。

  layout 文件的特殊性

  提到 xml 文件,条件反射般地就会想到是 IO 操作,性能差,这没错,但 layout 文件却比较特殊。在 Andorid 应用打包过程中,AAPT 会对资源进行打包,会将除了 asset 文件夹下的 xml 文件通过字符串池复用、二进制转换等方式进行压缩,最终生成压缩后的资源文件和资源文件索引 resources.arsc 还有 R 文件。而在使用 AssetManager 对资源文件进行加载时,我们也会使用 mmap 来降低 IO 成本。  

  通过分析以上种种手段的利弊,我们在实际应用场景中测试后发现读取 layout 文件的耗时通常不超过 1ms。因此,考虑到将 layout 文件的读取和解析移到编译阶段所带来的维护成本,权衡之下我们最终选择了直接放弃这一部分的优化。  

  在当下的开发环境中,Layout2Code 这一方案在性能提升方面仍然能够发挥很大的作用,当然有效使用这一方案的前提是开发者足够了解方案原理,以及知晓其具体的适用范围(非 AppComapt 组件)。

  相比于传统的 X2C 方案,Layout2Code 的适用范围更广,维护成本也更低。目前,该方案已经在小红书 APP 中得到了广泛的应用,并为我们带来了良好的收益和效果。我们对 Layout2Code 的研究由 kotlin 实现,使用 kapt,在未来我们也计划接入 ksp,来减少编译期耗时,持续优化这一方案。

0
相关文章