当前位置: 首页 > 科技观察

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

时间:2023-03-19 20:22:26 科技观察

2020年初,小红书首页UI的复杂度明显增加。在优化布局xml和使用一些存根方法的同时,我们也在寻找成本更低、性能更好的东西。X2C是当时业界知名的一种优化方式。它的原理是在编译时将xml翻译成代码,可以有效避免反射和读取资源文件的丢失。由于小红书APP中有很多自定义View的场景,X2C也会带来很高的维护成本。经过耗时深入分析LayoutInflater,我们找到了兼容各种View场景的APT方案。该方案既避免了反射带来的损失,又不增加额外的维护成本,成为开箱即用的工具。我们的探索受到了ViewCompiler的启发。ViewCompiler作为Google的一个实验性工具,可以手动将xml布局转换成java文件或者dex文件,但是不支持merge和include标签。ViewCompiler是在AndroidQ(Android10)中引入的,目前还是一个实验性的工具,所以我们平时没有办法使用它。下图是AndroidS(Android12)中的源码,可以看到这个功能没有开启。原理也很简单,先生成模板代码片段,再生成遍历xml的逻辑代码。这样做的主要好处是可以节省反射带来的时间消耗。官方的AppCompatViewInflater已经处理了原生View的创建。通过直接匹配新对象的名称,避免了使用反射带来的性能开销。在日常使用中,反射性能的开销主要集中在自定义View上。我们的App本身就是一个场景,自定义View比较多,自然适合VIewCompiler的这种方法。同时,因为在遍历xml的时候会遍历每一个attrs,所以在可维护性上也有很大的优势,我们不需要对自定义的attrs做任何处理。在阅读了X2C和ViewCompiler的源码和生成代码的基础上,我们决定做一个可以生成Kotlin代码的代码,同时解决ViewCompiler不支持的include和merge标签。我们使用的工具比较常规,包括kapt和kotlinpoet。总体思路是通过Resources.getLayout获取XmlResourceParser,然后通过parser的连续next遍历xml中的各个标签。生成的代码如下所示:当遇到merge和include时,我们需要对递归调用有特殊的处理逻辑,这样才能将父子布局链接在一起。在用这种新方法替换首页部分布局实现后,我们发现在线首页p90部分的布局时间减少了200ms+,时长、CES、留存等指标都有明显提升。LayoutInflater的工作过程LayoutInflater的工作过程可以简单地用下图表示:本文介绍的解决方案是在编译时使用apt生成代码。方便地解析布局文件后,我们使用生成的代码直接创建实例。AppCompat的基本组件逻辑后的效率和命中后的效率在理论上是一致的。AppCompat基础组件可以查看AppCompatViewInflater.java的源码(如上图部分所示),其中包含了TextView、Button等十几个常用的基础组件。就具体的布局而言,通过使用Layout2Code可以提升的性能只有基础组件以外的组件,尤其是当布局大量使用自定义组件时,效果尤为明显。这也给了我们另一个提醒。比如你在xml中写TextView/TextViewCompat,在AppCmpatViewInflater的作用下最终创建的实例就是TextViewCompat。但是当不使用Layout2Code或类似X2C的解决方案时,它们的效率是不同的。前者打上图直接创建逻辑,后者会通过反射创建。X2C的不足除了上述优化,X2C还将布局文件的读取和解析移至编译阶段,减少IO开销。但是编译时解析xml最大的难点在于我们需要将View的属性一个一个翻译出来。原因是编译时没有SDK依赖,所以不能生成AttributeSet对象供View构造函数直接消费。这样,需要手动维护翻译规则,将每一个xml属性转换成设置View属性的代码,带来了几个问题:1.生成的代码量成倍增长2.需要极高的维护成本支持3.有些xml属性没有对应的方法或者不是一一对应的。总而言之,要在此基础上保持稳健完整的功能是非常困难的。与我们探索的新的Layout2Code方案相比,它在兼容性和维护成本上有着巨大的优势。唯一需要考虑的权衡是有多少优化空间可用于在运行时读取布局文件,以及是否值得这样的投资。布局文件的特殊性说到xml文件,条件反射的认为是IO操作,性能很差,没错,但是布局文件还是比较特殊的。在Andorid应用打包过程中,AAPT会对资源进行打包,通过字符串池复用、二进制转换等方式对除asset文件夹外的xml文件进行压缩,最终生成压缩资源文件和资源文件索引资源.arsc和R文件。在使用AssetManager加载资源文件的时候,我们也会使用mmap来降低IO成本。通过分析以上几种方式的优劣,我们在实际应用场景中测试后发现,读取布局文件的耗时通常不会超过1ms。因此,考虑到将布局文件的读取和解析移至编译阶段带来的维护成本,我们最终在权衡下直接选择放弃这部分优化。在目前的开发环境下,Layout2Code方案还是可以起到很大的性能提升作用的。当然,有效使用该方案的前提是开发者对该方案的原理及其具体适用范围(非AppComapt组件)有足够的了解。与传统的X2C方案相比,Layout2Code适用范围更广,维护成本更低。目前该方案已经在小红书APP中得到广泛应用,给我们带来了很好的收益和效果。我们研究Layout2Code是用kotlin实现的,用的是kapt。未来我们也计划接入ksp来减少编译耗时,继续优化这个方案。无耻(blv@xiaohongshu.com)小红书商业技术Android工程师凌人(lingren@xiaohongshu.com)小红书商业技术Android工程师