草庐IT

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

殇不患 绫人 2023-03-28 原文

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

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

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

1、方案的探索

我们的探索灵感来自于 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、留存等指标均得到了显著提升。

2、探入分析

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 文件的读取和解析移到编译阶段所带来的维护成本,权衡之下我们最终选择了直接放弃这一部分的优化。

3、总结

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

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

4、作者简介

殇不患 (blv@xiaohongshu.com) 小红书商业技术 Android 工程师

绫人(lingren@xiaohongshu.com) 小红书商业技术 Android 工程师

有关高兼容低成本,开箱即用的首页性能优化方式被我们找到了的更多相关文章

  1. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  2. Ruby 的数字方法性能 - 2

    我正在使用Ruby解决一些ProjectEuler问题,特别是这里我要讨论的问题25(Fibonacci数列中包含1000位数字的第一项的索引是多少?)。起初,我使用的是Ruby2.2.3,我将问题编码为:number=3a=1b=2whileb.to_s.length但后来我发现2.4.2版本有一个名为digits的方法,这正是我需要的。我转换为代码:whileb.digits.length当我比较这两种方法时,digits慢得多。时间./025/problem025.rb0.13s用户0.02s系统80%cpu0.190总计./025/problem025.rb2.19s用户0.0

  3. ruby - Ruby 性能中的计时器 - 2

    我正在寻找一个用ruby​​演示计时器的在线示例,并发现了下面的代码。它按预期工作,但这个简单的程序使用30Mo内存(如Windows任务管理器中所示)和太多CPU有意义吗?非常感谢deftime_blockstart_time=Time.nowThread.new{yield}Time.now-start_timeenddefrepeat_every(seconds)whiletruedotime_spent=time_block{yield}#Tohandle-vesleepinteravalsleep(seconds-time_spent)iftime_spent

  4. ruby-on-rails - 如果条件与 &&,是否有任何性能提升 - 2

    如果用户是所有者,我有一个条件来检查说删除和文章。delete_articleifuser.owner?另一种方式是user.owner?&&delete_article选择它有什么好处还是它只是一种写作风格 最佳答案 性能不太可能成为该声明的问题。第一个要好得多-它更容易阅读。您future的自己和其他将开始编写代码的人会为此感谢您。 关于ruby-on-rails-如果条件与&&,是否有任何性能提升,我们在StackOverflow上找到一个类似的问题:

  5. ruby-on-rails - 不兼容的库版本 : nokogiri. bundle 需要 8.0.0 或更高版本,但 libiconv.2.dylib 提供 7.0.0 版本 - 2

    为了在我的mac上为一个rails项目安装mysql,我遵循了安装Homebrew软件和删除mac端口的在线建议。这是问题开始的地方。rails项目不会构建,我得到这个:[rake--prereqs]rakeaborted!dlopen(/Users/Parker/.rvm/gems/ruby-1.9.3-p448/gems/nokogiri-1.6.0/lib/nokogiri/nokogiri.bundle,9):Librarynotloaded:/opt/local/lib/libiconv.2.dylibReferencedfrom:/Users/Parker/.rvm/gem

  6. ruby-on-rails - 在 Rails 3 中进行身份验证最常用的方法是什么? - 2

    我需要在rail3中使用标准注册/登录/忘记密码功能进行身份验证。是否有大多数人为此使用的插件或其他东西? 最佳答案 我不确定最常用的方法是什么-但可以肯定的是,Plataformatec的“Devise”是一个非常流行的方法:http://github.com/plataformatec/devise我已经尝试了一些authgem,对我来说,它是最简单的设置和修改以满足我的需要。它内置了密码恢复、帐户确认(如果需要)和其他一些非常方便的功能。 关于ruby-on-rails-在Rail

  7. ruby-on-rails - 如何在 Rails 中添加禁用的提交按钮 - 2

    我在ruby​​表单中有一个提交按钮f.submitbtn_text,class:"btnbtn-onemgt12mgb12",id:"btn_id"我想在不使用任何javascript的情况下通过ruby​​禁用此按钮 最佳答案 添加disabled:true选项。f.submitbtn_text,class:"btnbtn-onemgt12mgb12",id:"btn_id",disabled:true 关于ruby-on-rails-如何在Rails中添加禁用的提交按钮,我们在St

  8. ruby - 如何保持我不常用的编程语言技能 - 2

    关闭。这个问题是off-topic.它目前不接受答案。想改进这个问题吗?Updatethequestion所以它是on-topic用于堆栈溢出。关闭11年前。Improvethisquestion我不经常使用ruby​​-通常它加起来相当于每两个月或更长时间编写一次脚本。我的大部分编程都是使用C++进行的,这与ruby​​有很大不同。由于我与ruby​​之间的差距如此之大,我总是忘记语言的基本方面(比如解析文本文件和其他简单的东西)。我想每天练习一些基本的东西,我想知道是否有一些我可以订阅的网站,并且会向我发送当天的Ruby问题或类似的东西。有人知道这样的站点/Internet服务吗?

  9. ruby - 如何找到我的 Ruby 应用程序中的性能瓶颈? - 2

    我编写了一个Ruby应用程序,它可以解析来自不同格式html、xml和csv文件的源中的大量数据。我如何找出代码的哪些区域花费的时间最长?有没有关于如何提高Ruby应用程序性能的好资源?或者您是否有任何始终遵循的性能编码标准?例如,你总是用加入你的字符串吗?output=String.newoutput或者你会使用output="#{part_one}#{part_two}\n" 最佳答案 好吧,有一些众所周知的做法,例如字符串连接比“#{value}”慢得多,但是为了找出您的脚本在哪里消耗了大部分时间或比所需时间更多,您需要进行分

  10. STM32的HAL和LL库区别和性能对比 - 2

    LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L

随机推荐