前言
Jetpack Compose 在使用的过程中,我感觉性能不如传统 View。
主要有以下几方面原因:
编译和构建时间:Compose 编译器会处理应用中的可组合项,这会增加应用的构建时间。平均构建时间从 299 毫秒增加到 399 毫秒。虽然完成迁移后构建时间有所缩短,但在开发过程中,较长的构建时间可能会让开发者觉得性能不佳。
内存占用:Jetpack Compose 需要增加依赖,这会增加包的占用空间和运行时的内存使用。而 View 系统是 Android 系统的一部分,不需要额外的导入,在内存占用方面可能更有优势。
重组开销:当界面数据发生变更时,Compose 需要重新组合受影响的项目。
在处理大规模列表或复杂 UI 结构时,更多的项目需要重组,从而带来较大的 CPU 和内存开销。
例如,Compose 中的 LazyColumn 在渲染大量数据时,其重组开销可能会比传统 View 中的 RecyclerView 大。
绘制效率:Compose 需要在低层次 API 上反复执行测量和布局操作,这可能会影响最终渲染速度。
而传统 View 系统中的 RecyclerView 的 ViewHolder 模型在绘制效率上有一定优势,尤其是在复杂布局场景下,ViewHolder 的扁平视图层级结构可以避免一些内部的测量和布局开销。
版本和优化程度:View 系统已经存在多年,经过了大量的优化,对于复杂布局的性能表现通常较为稳定。
而 Jetpack Compose 相对较新,虽然每次迭代都在改进,但在某些特定场景下,可能还没有达到 View 系统的优化程度。
优化方式
延迟加载数据(效果显著)
加载数据后页面就要开始渲染,这时候可能页面的跳转动画还没完成,就会导致卡顿。
添加延迟是最有效的优化手段。
1 | LaunchedEffect(Unit) { |
稳定的类
不要在状态持有类中使用 var 修饰属性
当属性是可变的,但不通知composition ,这会导致使用它们的可组合项变得不稳定。
应该这样写:
1 | data class InherentlyStableClass(val text: String) |
不应该这样写:
1 | data class InherentlyUnstableClass(var text: String) |
尽可能拆分
1 |
|
在这个例子中,ProfileScreen
将 ProfileHeader
和 ProfileContent
组件拆分开来。
当用户数据更新时,Compose 仅重组更新的组件,而不会影响其他不相关的组件。
如果写在一个组件中会触发整体的重组。
使用remember
remember
是 Compose 中非常重要的一个工具,它可以帮助开发者在重组期间保存组件的状态。
通过将状态存储在 remember
中,我们可以避免在每次重组时重新创建不必要的对象。
示例代码:
1 |
|
状态优化
1 | // 缺点:每次重新组合时都重新计算 |
key
key
用于在列表、循环或动态组件中标记组件的唯一性,类似 RecyclerView 中的itemId
。
若 key
参数值未变化,即使父组件重组,子组件也会被尽可能复用(保留状态和内部状态)。
当 key()
的值变化时:
- 直接子组件会被销毁并重建(必然重组) (执行其
onDispose
逻辑)。 - 该子组件的所有后代组件(子组件的子组件)也会随之重组,无论其参数是否变化。
key()
的核心作用是 “控制其包裹范围是否因 key
变化而整体重组”,但无法阻止组件因 “自身输入参数变化” 或 “依赖的 State
变化” 而触发的重组。
1 |
|
预编译 ProfileInstaller
https://developer.android.google.cn/jetpack/androidx/releases/profileinstaller?hl=zh-cn
这种方式能大幅提升程序的运行性能,效果非常明显。
ART 默认情况下,并没有把 Compose 的核心代码进行AOT(预编译)
,而是JIT(即时编译)
执行。这就要命了,像 Compose 底层的 Snapshot 系统、Slot Table,都是热点代码,短时间内会被频繁调用,JIT 根本无法满足 Compose 的性能要求。
ProfileInstaller为什么会提升性能
本来应用在启动的时候系统会进行预编译,但是在国内修改的系统中,不会进行预编译。
ProfileInstaller
能触发程序的预编译,并且在应用安装后、首次启动前,提前将预编译好的 Profile 注入到应用中,跳过动态生成步骤,直接复用预编译配置。
ProfileInstaller
依赖 Android 8.0 以上的私有目录权限和文件系统特性,低于 API 26 的设备无法使用。
添加依赖
1 | implementation("androidx.profileinstaller:profileinstaller:1.4.1") |
引用初始化的时候添加
在Application中添加
1 | if (!BuildConfig.DEBUG) { |
注意
要在release环境中调用,debug调用程序会崩溃。
状态和稳定性
在 Jetpack Compose 中,“状态变化” 是触发重组的直接原因,但 “稳定性” 决定了重组的范围和效率—— 两者并非孤立,而是协同影响 Compose 的渲染行为。
状态变化是重组的 “触发器”
Compose 的核心是 “状态驱动 UI”,只有当可观察状态(如 mutableStateOf
、StateFlow
等)发生变化时,才会主动触发重组流程。
稳定性的作用:决定 “重组会影响哪些子组件”
当状态变化触发重组后,Compose 需要判断:哪些子组件需要跟着重组?
这正是 “稳定性” 发挥作用的地方 —— 它决定了 Compose 对 “参数是否真的变化” 的判断,从而避免不必要的重组扩散。
重组的前提
Compose 之所以能实现 “状态驱动的局部更新”,本质是通过对比组件的 “输入参数” 和 “依赖状态” 来判断是否需要重组:
- 当组件的输入参数(如函数参数、
remember
缓存的值)或依赖的State
(如mutableStateOf
)发生变化时,Compose 会认为 “组件需要更新”,触发重组; - 若输入参数和依赖状态未变,Compose 会跳过重组,直接复用之前的渲染结果。
Compose 都是函数,是否重组是由参数决定的。
在 Jetpack Compose 中,函数参数为数字(如 Int
、Long
)或字符串(String
)、或者对象时,
其值变化是否触发重组,取决于两个核心条件:
- 参数是否为
可跟踪的状态
或者是可跟踪状态的委托
。 - 值的实际内容是否发生变化。
核心结论
若参数是“普通变量”(非状态类型):
即使值变化,也不会触发重组。因为 Compose 不会跟踪普通变量的变化,仅依赖“状态驱动”机制。若参数是“可跟踪的状态”(如
State<T>
包装):
当值的实际内容发生变化时,会触发重组;若值未变化(如从5
变为5
),则不会触发重组。
详细分析
参数是普通变量(非状态)
普通的数字/字符串变量(未用 mutableStateOf
等包装)属于“不可跟踪”的参数。
即使值变化,Compose 感知不到,因此不会触发重组。
1 |
|
参数是可跟踪的状态
参数是可跟踪的状态(State<T>
)
当参数是 State<Int>
、State<String>
等可跟踪类型时,Compose 会自动监听其变化:
- 若值的内容确实变化(如
3
→4
,"a"
→"b"
),则触发重组; - 若值未变化(如
5
→5
,"test"
→"test"
),则不触发重组(优化机制)。
1 |
|
参数是状态的“解包值”
在 Compose 中,当使用 by
关键字解包 State
(如 var count by mutableStateOf(0)
),变量 count
本质仍是可跟踪状态的“代理”。
此时将其作为参数传递,效果与传递 State<T>
一致:值变化时触发重组。
1 |
|
稳定性
@Immutable和@Stable
Jetpack Compose 中的稳定性(Stability)
Compose 编译器通过分析对象的稳定性来决定是否跳过重组(Recomposition)。
如果一个对象被认为是“稳定”的,那么当它作为参数传入到 Composable 函数中时,只要引用没有变化,Compose 就可以跳过对该函数的重组。
Compose 编译器会根据以下规则判断一个类是否稳定:
判断条件 | 是否稳定 |
---|---|
所有属性是 val,且类型稳定 | ✅ 是(默认 @Immutable) |
使用了 @Stable 注解 | ✅ 视为稳定 |
包含 mutableStateOf 或其他可观察类型 | ✅ 可追踪变化 |
属性是 var,未使用任何注解 | ❌ 不稳定 |
@Immutable
表示这个类是完全不可变的(immutable),所有属性都是 val 且不会改变。
Compose 编译器会认为这样的对象在整个生命周期内都不会发生变化。
特点: 所有字段必须是 val。 推荐用于数据模型类(如:用户信息、配置等)。 适用于纯数据类,比如 Kotlin 的 data class。 可以安全地跳过重组,提高性能。
1 |
|
@Stable
表示这个类是“稳定的”,但允许包含可变字段(如 var),前提是这些字段的变化不会影响 UI 的状态,或者你已经通过 Compose 的响应式机制(如 mutableStateOf)管理了其变化。
特点: 允许使用 var。 需要开发者自己保证类的行为符合“稳定性”要求。 如果某个字段确实会变化但你不希望触发重组,可以用 @Stable 告诉 Compose 忽略它。
如果你想让某些字段变化也能触发重组,可以在这些字段中使用 mutableStateOf()。
1 |
|
count
是 var
,但它是一个 mutableStateOf
,Compose 能追踪它的变化并触发重组。
整个 Counter
类被标记为 @Stable
,
Compose 认为它是稳定的,不会因为 name 改变而进行重组,因为 Compose 不会追踪它的变化,
除非 count
改变,否则不会重组。