前言
最近Compose
已经正式发布了1.0
版本,这说明谷歌认为Compose
已经可以用于正式生产环境了 那么相比传统的XML
,Compose
的性能到底怎么样呢?
本文主要从构建性能与运行时两个方面来分析Compose
的性能,数据主要来源于:Jetpack Compose — Before and after[1] 与 Measuring Render Performance with Jetpack Compose[2] , 想了解更多的同学可以直接点击查看
构建性能
Compose
构建性能主要以 tivi[3] 为例来进行说明 Tivi
是一个开源的电影App
,原本基于Fragment
与XML
构建,同时还使用了DataBinding
等使用了注解处理器的框架 后来迁移到使用Compose
构建UI
,迁移过程分为两步
- 第一步:迁移到
Navigation
与Fragment
,每个Fragment
的UI
则由Compose
构建 - 第二步:移除
Fragment
,完全基于Compose
实现UI
下面我们就对Pre-Compose
,Fragments + Compose
,Entirely Compose
三个阶段的性能进行分析对比
APK
体积
包体积是我们经常关注的性能指标之一,我们一起看下3个阶段的包体积对比
可以看出,Tivi
的 APK
大小缩减了 46%
,从 4.49MB
缩减到 2.39MB
,同时方法数也减少了17%
值得注意的是,在刚开始在应用中采用Compose
时,有时您会发现APK
大小反而变大了 这是因为迁移没有完成,老的依赖没有完成移除,而新的依赖已经添加了,导致APK
体积变大 而在项目完全迁移到Compose
后,APK
大小会减少,并且优于原始指标。
代码行数
我们知道在比较软件项目时,代码行数并不是一个特别有用的统计数据,但它确实提供了对事物如何变化的一个观察指标。 我们使用cloc工具[4]来计算代码行数
cloc . --exclude-dir=build,.idea,schemas
结果如下图所示:
可以看出,在迁移到Compose
后,毫无意外的,XML
代码行减少了76%
有趣的是kotlin
代码同样减少了,可能是因为我们可以减少很多模板代码,同时也可以移除之前写的一些View Helper
代码
构建速度
随着项目的不断变大,构建速度是开发人员越来越关心的一个指标。 在开始重构之前,我们知道,删除大量的注解处理器会有助于提高构建速度,但我们不确定会有多少。
我们运行以下命令5次,然后取平均值
./gradlew --profile --offline --rerun-tasks --max-workers=4 assembleDebug
结果如下
这里考虑的是调试构建时间,您在开发期间会更关注此时间。
在迁移到Compose
前,Tivi
的平均构建时间为 108.71
秒。 在完全迁移到 Compose
后,平均构建时间缩短至 76.96
秒!构建时间缩短了 29%
。 构建时间能缩短这么多,当然不仅仅是Compose
的功劳,在很大程度上受两个因素的影响:
- 一个是移除了使用注解处理器的
DataBinding
和Epoxy
- 另一个是
Hilt
在AGP 7.0
中的运行速度更快。
运行时性能
上面我们介绍了Compose
在构建时的性能,下面来看下Compose
在运行时渲染的性能怎么样
分析前的准备
使用Compose
时,可能有多种影响性能的指标
- 如果我们完全在
Compose
中构建UI
会怎样? - 如果我们对复杂视图使用
Compose
(例如用LazyColumn
替换RecyclerViews
),但根布局仍然添加在XML
中 - 如果我们使用
Compose
替换页面中一个个元素,而不是整个页面,会怎么样? - 是否可调试和
R8
编译器对性能的影响有多大?
为了开始回答这些问题,我们构建了一个简单的测试程序。 在第一个版本中,我们添加了一个包含50个元素的列表(其中实际绘制了大约 12 个)。该列表包括一个单选按钮和一些随机文本。
为了测试各种选项的影响,我们添加以下4种配置,以下4种都是开启了R8
同时关闭了debug
纯Compose
- 一个
XML
中,只带有一个ComposeView
,具体布局写在Compose
中 XML
中只包含一个RecyclerView
,但是RecyclerView
的每一项是一个ComposeView
- 纯
XML
同时为了测试build type
对性能的影响,也添加了以下3种配置
- 纯
Compose
,关闭R8
并打开debug
- 纯
Compose
,关闭R8
并关闭debug
- 纯
XML
,关闭R8
并打开debug
如何定义性能?
Compose
运行时性能,我们一般理解的就是页面启动到用户看到内容的时间 因此下面几个时机对我们比较重要
Activity
启动时间,即onCreate
Activity
启动完成时间,即onResume
Activity
渲染绘制完成时间,即用户看到内容的时间
onCreate
与onResume
的时机很容易掌握,重写系统方法即可,但如何获得Activity
完全绘制的时间呢? 我们可以给页面根View
添加一个ViewTreeObserver
,然后记录最后一次onDraw
调用的时间
使用Profile
查看上面说的过程,如下分别为使用XML
渲染与使用Compose
渲染的具体过程,即从OnCreate
到调用最后一次onDraw
的过程
渲染性能分析
知道了如何定义性能,我们就可以开始测试了
- 每次测试都在几台设备上运行,包括最近的旗舰、没有
Google Play
服务的设备和一些廉价手机。 - 每次测试在同一台手机上都会运行10次,因此我们不仅可以获取首次渲染时间,也可以获取二次渲染时间
- 测试
Compose
版本为1.0.0
我们根据上面定义的配置,重复跑了多次,得到了一些数据,感兴趣的同学可以直接查看所有数据[5]
分析结果如上图所示,我们可以得出一些结论
R8
和是否可调试对Jetpack Compose
渲染时间产生了显着影响。在每次实验中,禁用R8
和启用可调试性的构建所花费的时间是没有它们的构建的两倍多。在我们最慢的设备上,R8
将渲染速度加快了半秒以上,而禁用debug
又使渲染速度加快了半秒。XML
中只包含一个ComposeView
的渲染时间,跟纯Compose
的耗时差不多RecyclerView
中包含多个ComposeView
是最慢的。这并不奇怪,在XML
中使用ComposeView
是有成本的,所以页面中使用的ComposeView
越少越好。XML
在呈现方面比Compose
更快。没有办法解决这个问题,在每种情况下,Compose
的渲染时间比XML
长约 33%。- 第一次启动总是比后续启动花费更长的时间来渲染。如果您查看完整的数据,第一个页面的渲染时间几乎是后续的两倍。
比较让我惊讶的是,尽管Compose
没有了将XML
转化成View
的IO
操作,测量过程也因为固有特性测量提高了测量效率,但性能仍然比XML
要差 不过,根据`Leland Richardson`[6]的说法[7],当从Google Play
安装应用程序时,由于捆绑的AOT
编译,Compose
在启动时渲染会更快,从而进一步缩小了与XML
的差距
总结
经过上面对Compose
性能方面的分析,总结如下
- 如果完全迁移到
Compose
,在包体积,代码行数,编译速度等方面应该会有比较大的改善 - 如果处于迁移中的阶段,可能因为旧的依赖没有去除,而已经引入了新的依赖,反而导致包体积等变大
- 尽管没有了
XML
转换的IO
操作,测量过程也通过固有特性测量进行了优化,Compose
的渲染性能比起XML
仍然有一定差距 - 尽管目前
Compose
在性能方面略有欠缺(在大多数设备上仅超过一两帧),但由于其在开发人员生产力、代码重用和声明式UI
的强大特性等方面的优势,Compose
仍被推荐使用