前言
Jetpack Compose 通过声明式编程和数据驱动 UI 简化了 Android 开发。然而,高效利用其性能潜力需要深刻理解重组------即当状态变化时更新 UI 的过程。本文旨在深入探讨 Compose 的重组机制,并提供切实可行的优化策略,以避免常见性能陷阱,构建更流畅的应用。
一、Jetpack Compose 重组机制
Compose 会通过几个不同的阶段来渲染帧。比如Android View 系统有 3 个主要阶段:测量、布局和绘制。而Compose 和它非常相似,但开头多了一个叫做"组合"的重要阶段。
官方有一张图片很清楚表述了上述内容,我贴在下面:
Compose的 3 个主要阶段
- 组合:要显示什么样的界面。Compose 运行可组合函数并创建界面说明。
- 布局:要放置界面的位置。该阶段包含两个步骤:测量和放置。一般是父布局首先将自己的约束传给子布局,子布局测量自己,再将大小传给父布局,父布局再进行放置。(但另一些布局如LazyColumn不是这样。因为着重介绍重组性能优化,此处不展开)
- 绘制:渲染的方式。界面元素会绘制到画布(通常是设备屏幕)中。

二、Compose 重组优化策略
2.1 减小重组的范围
2.1.1 跳过可组合函数重组
可组合函数是否可跳过可以看下面的流程图
Compose跳过单个可组合函数重组的5个核心条件
- 调用点位置不变(第一个判断节点,"否"分支继续);
- 返回类型为
Unit
(第二个判断节点,"是"分支继续); - 无禁止跳过的注解(第三个判断节点,"否"分支继续);
- 所有参数类型稳定(第四个判断节点,"是"分支继续;"否"分支需检查是否被标记为稳定);
- 参数值未更改(最后一个判断节点,"是"分支跳过重组)。
如果某种类型要被视为稳定类型,则必须符合以下协定:
- 对于相同的两个实例,其
equals
的结果将始终相同。** - 如果类型的某个公共属性发生变化,组合将收到通知。
- 所有公共属性类型也都是稳定。
除了上面的条件,还有一些是Compose 编译器直接视为稳定的类型:
- 所有基元值类型:
Boolean
、Int
、Long
、Float
、Char
等。 - 字符串
- 所有函数类型 (lambda)
上面这些都比较好理解,着重讲下调用点和lambda参数
调用点
为了更直观地理解调用点的概念,我们用一个生活化的例子说明:
我们可以将界面UI的构成比作一辆长途客车:
- 可组合函数 就像是客车上的座位 。它们的位置(在代码树中的位置) 和类型(是Text还是Button) 是相对固定的。
- 每次组合时,生成并显示在座位上的数据(状态) 就像是乘客。
重组(Recomposition) 就像是乘务员核对并更新乘客信息:
- 乘客信息变化了 (状态变化)-> 乘务员更新该座位对应的乘客信息表 -> 重组这个座位(可组合项) 。
- 座位本身发生了变化 (调用点变化,例如在
if
语句中动态插入或移除了一个可组合项)-> 整辆客车的座位布局关系变了 -> 乘务员需要更新整个座位关系表,并可能引发一系列座位的核对 -> 发生重组。
跳过重组就像是:某个座位上的乘客信息没变,且这个座位在客车中的位置也没变 -> 乘务员就跳过这个座位的核对。
乘务员如何工作:
- 乘客信息没有更新,位置关系也没变化 -> 调用点、输入信息均无变化 -> 跳过重组
- 乘客信息没有更新,但位置关系变化了 -> 调用点变化 -> 执行重组
- 乘客信息变化,位置关系没变化 -> 输入信息更改 -> 执行重组
lambda参数
使用lambda参数有一点需要注意的地方,我们直接看一个简单的栗子
在页面上现在有红绿蓝3个按钮,按钮上面有一个色块用来显示更新的颜色,默认为红色。

代码如下:
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
...
val viewModel by viewModels<MainActivityViewModel>()
RgbSelector(color = viewModel.color, onColorClick = {
viewModel.changeColor(it)
})
...
}
}
}
kotlin
class MainActivityViewModel : ViewModel() {
var color by mutableStateOf(Color.Red)
private set
fun changeColor(color: Color) {
this.color = color
}
}
kotlin
@Composable
fun RgbSelector(
color: Color,
onColorClick: (Color) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier
.size(100.dp)
.background(color))
Spacer(modifier = Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Button(
onClick = {
onColorClick(Color.Red)
}) {
Text(text = "Red")
}
Button(onClick = {
onColorClick(Color.Green)
}) {
Text(text = "Green")
}
Button(onClick = {
onColorClick(Color.Blue)
}) {
Text(text = "Blue")
}
}
}
}
运行上面代码,点击绿色或蓝色按钮时,色块和所有按钮都会触发重组,但理想情况下,只需重组色块和被点击的按钮。
原因就是lambda参数传了{ viewModel.changeColor(it) }:
- Kotlin编译器会将lambda函数编译为简单的匿名类
- ViewModel会作为参数传递给这个匿名类
- ViewModel因为是一个复杂对象,不会被标记为稳定类
具体看看{ viewModel.changeColor(it) }对应的Java代码:
csharp
new Function1() { // 无固定 receiver,每次创建新实例
public void invoke_8_81llA(long it) {
null.invoke$lambda$0(viewModel$delegate).changeColor-8_81llA(it); // 通过委托访问 viewModel
}
}
- Lambda 捕获了
viewModel
变量(即使 viewModel 是 val,但其指向的对象可能变化)。 - 每次父组件重组时,都会创建新的 Function1 实例(因为 lambda 重新声明)。
- Compose 将其视为 不稳定参数。
所以{ viewModel.changeColor(it) }每次重新组合时都会被视为一个新的实例,也就导致了RgbSelector不是全部参数类型稳定,然后整个RgbSelector都重组了。
下面是Layout Inspector查看RgbSelector重组情况。右下3个按钮组合次数都+1
修改方案有2种
- 使用函数引用(Function Reference)语法,直接传递方法引用
- 使用remember
传递方法引用
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
...
val viewModel by viewModels<MainActivityViewModel>()
RgbSelector(color = viewModel.color, onColorClick = viewModel::changeColor)
...
}
}
}
对应的Java代码:
csharp
new Function1(invoke$lambda$0(viewModel$delegate)) { // 使用固定 receiver(viewModel 实例)
public void invoke_8_81llA(long p0) {
((MainActivityViewModel) this.receiver).changeColor-8_81llA(p0); // 直接调用固定函数
}
}
- viewModel::changeColor 是一个 函数引用,指向viewModel的固定方法。
- 编译后生成单例Function1对象,实例不会变化(即使父组件重组)。
- Compose 将其视为 稳定参数。
下面是Layout Inspector查看RgbSelector重组情况,右下仅点击按钮组合次数+1
使用remember
对于无法使用 :: 的方式传入Lambda参数,可以使用remember
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
...
val viewModel by viewModels<MainActivityViewModel>()
val changeColorLambda = remember<(Color) -> Unit> {
{
viewModel.changeColor(it)
}
}
RgbSelector(color = viewModel.color, onColorClick = changeColorLambda)
...
}
}
}
对应的Java代码:
ini
// 检查是否已有记忆值
if (it$iv$iv == Composer.Companion.getEmpty()) {
// 首次组合:创建新的 Function1 实例
Object value$iv$iv = new Function1() {
public void invoke_8_81llA(long it) {
null.invoke$lambda$0(viewModel$delegate).changeColor-8_81llA(it);
}
public Object invoke(Object p1) {
this.invoke-8_81llA(((Color)p1).unbox-impl());
return Unit.INSTANCE;
}
};
// 存储实例以便后续重组复用
$composer.updateRememberedValue(value$iv$iv);
var10000 = value$iv$iv;
} else {
// 后续重组:复用已存储的实例
var10000 = it$iv$iv;
}
// 获取记忆的 lambda 实例
Function1 changeColorLambda = (Function1)var14;
// 传递稳定的实例给 RgbSelector
RgbSelector(color = viewModel.color, onColorClick = changeColorLambda)
用 remember 让 lambda 参数为稳定类型的原因:重组会使用之前的实例
下面是Layout Inspector查看RgbSelector重组情况,右下仅点击按钮组合次数+1 在Android Studio中,我们可以使用Compose编译器报告,来快速查看哪些可组合函数是稳定/不稳定的
2.1.2 在列表中使用 key
在LazyColumn
等列表中,为每一项提供一个唯一且稳定的key
。这帮助Compose在数据集变化(如排序、增删)时,准确识别出哪些项是新增、移动或移除的,从而重用现有组件实例,避免不必要的重组。
举个栗子
kotlin
@Composable
fun LazyColumnItem(modifier: Modifier = Modifier) {
var items by remember { mutableStateOf(listOf(1, 2, 3)) }
Column(modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
LazyColumn {
items(items) { item ->
Text("Item $item")
}
}
Button(onClick = {
items = listOf(items.size + 1) + items
}) { Text("add item") }
}
}
点击按钮,添加一个Text 看看不使用Key重组情况 可以看到,每次点击按钮,之前的几个Text也会跟着重组。
下面再看看使用Key
kotlin
@Composable
fun LazyColumnItem(modifier: Modifier = Modifier) {
var items by remember { mutableStateOf(listOf(1, 2, 3)) }
Column(modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
LazyColumn {
items(items, key = { it }) { item -> // 1
Text("Item $item")
}
}
Button(onClick = {
items = listOf(items.size + 1) + items
}) { Text("add item") }
}
}
只是在代码1处添加了key参数 可以看到,已经存在的Text,会自动跳过组合阶段的重新执行
2.1.3 状态下沉
原理: 状态下沉通过将状态移至实际使用它的最小可组合函数范围内,实现:
- 调用点保持不变:父组件调用结构不受影响,位置稳定性得以保留。
- 输入变化精准定位:状态变化被隔离在子组件内部,父组件输入参数未变。
- 跳过不必要的重组判断 :
- 父组件满足调用点不变 + 输入未变 → 直接跳过整个父组件的重组
- 子组件因直接绑定状态 → 仅执行必要的重组"
举个栗子
kotlin
@Composable
fun ListItem(title: String) {
var isClicked by remember { mutableStateOf(false) } // 状态放在ListItem(高层组件)
Column {
Text(text = title) // 不变的部分
Button(onClick = { isClicked = !isClicked }) {
Text(if (isClicked) "Clicked" else "Click Me") // 变化的部分
}
}
}
当点击按钮时,整个Column的作用域都会重新执行,但实际上只有Button需要重新执行,可以将isClicked下沉到Button中,从而优化重组性能
kotlin
@Composable
fun ListItem(title: String) {
Column {
Text(text = title) // 不变的部分
ClickableButton() // 状态下沉到该子组件
}
}
@Composable
fun ClickableButton() {
var isClicked by remember { mutableStateOf(false) } // 状态放在需要的子组件
Button(onClick = { isClicked = !isClicked }) {
Text(if (isClicked) "Clicked" else "Click Me") // 仅此处重组
}
}
2.2 优化重组阶段
Compose的重组分为组合、布局、绘制三个阶段。我们应该尽量将工作从组合阶段转移到布局或绘制阶段。
2.2.1 使用Lambda处理动态Modifier
很多Modifier(如offset
、graphicsLayer
)接受Lambda表达式。如果其值是动态的,使用Lambda形式可以让计算从组合阶段推迟到布局/绘制阶段。

举个栗子
kotlin
@Composable
fun OffsetItem() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
TestWithoutLambda()
TestWithLambda()
}
}
@Composable
fun TestWithoutLambda() {
var offsetX by remember { mutableIntStateOf(0) }
Text(
text = "hello world",
modifier = Modifier.offset(x = offsetX.dp, y = 0.dp)
)
Button(onClick = { offsetX += 100 }) { Text("Move") }
}
@Composable
fun TestWithLambda() {
var offsetX by remember { mutableIntStateOf(0) }
Text(
text = "hello world",
modifier = Modifier.offset { IntOffset(offsetX, 0) }
)
Button(onClick = { offsetX += 100 }) { Text("Move") }
}
在Column中分别使用offset的非Lambda和Lambda方式,点击按钮,偏移Text
使用Layout Inspector查看重组阶段组合的次数,可以看到使用Modifier.offset()时,Button和Text组合次数均+1,而offset使用Lambda方式则只有Button组合次数+1,
Text
成功地跳过了本次重组的组合阶段。
我们来看看源码,为什么使用Modifier(如offset
)的Lambda能跳过组合阶段
kotlin
fun Modifier.offset(offset: Density.() -> IntOffset) =
this then
OffsetPxElement(
offset = offset,
...
)
private class OffsetPxElement(
val offset: Density.() -> IntOffset,
val rtlAware: Boolean,
val inspectorInfo: InspectorInfo.() -> Unit,
) : ModifierNodeElement<OffsetPxNode>() {
override fun create(): OffsetPxNode {
return OffsetPxNode(offset, rtlAware) // 1
}
...
}
private class OffsetPxNode(var offset: Density.() -> IntOffset, var rtlAware: Boolean) :
LayoutModifierNode, Modifier.Node() {
...
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
val offsetValue = offset() // 2
...
}
}
}
Modifier.offset{}实现类是OffsetPxNode,将Lambda表达式传到了MeasureScope.measure中才执行,也就是布局阶段才会执行Lambda。
2.3 减少非必要的重组次数
2.3.1 避免重组循环
重组循环: 在重组过程中修改状态 → 导致输入变化 → 触发新一轮重组 我们直接看官网的栗子:
kotlin
Box {
var imageHeightPx by remember { mutableIntStateOf(0) }
Image(
painter = painterResource(R.drawable.rectangle),
contentDescription = "I'm above the text",
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { size ->
// Don't do this
imageHeightPx = size.height
}
)
Text(
text = "I'm below the image",
modifier = Modifier.padding(
top = with(LocalDensity.current) { imageHeightPx.toDp() }
)
)
}
在第一帧的组合阶段,imageHeightPx
最初为 0
。因此,该代码会提供带有 Modifier.padding(top = 0)
的文本。后续的布局阶段会调用 onSizeChanged
修饰符的回调,该回调会将 imageHeightPx
更新为图片的实际高度。这会更新 imageHeightPx 的值,从而发生第二帧,也就触发新一轮重组。
这是一个典型的重组循环案例。在实际开发中,应尽量避免这种在布局阶段回调中修改状态触发新一轮组合的模式。更推荐使用 Compose 的布局系统本身来解决这类问题,例如使用 Column
的天然流式布局特性,或者利用 Modifier.weight()
等来动态分配空间,从而无需手动计算和设置高度/间距。
2.3.2 使用derivedStateOf合并高频状态更新
使用场景:状态或键的变化超过想要更新 UI 时
举个栗子:向下滚动,列表的第一项已经不可见时,在滚动列表右侧显示一个按钮
kotlin
@Composable
fun DerivedStateOfItem(modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
Row {
LazyColumn(state = listState) {
items(list, key = { it }) { item ->
Text("Item $item", modifier = modifier.height(150.dp))
}
}
val showButton = listState.firstVisibleItemIndex > 0
// Log.d("DerivedStateOfItem", "ComposeRecomposition")
if (showButton) {
Button(onClick = {}) {
Text("button")
}
}
}
}
可以从上图看出,每次向下滚动一个子项是,都会发生重组。也可以log日志打开,每次向下滚动一个子项,都会打印一次日志。
但实际上我们列表的第一项已经不可见时重组,后续向下滚动都不需要重组了。这里就是状态变化超过想要更新 UI。
使用derivedStateOf修改
kotlin
@SuppressLint("FrequentlyChangingValue")
@Composable
fun DerivedStateOfItem(modifier: Modifier = Modifier) {
...
val showButton by remember {
derivedStateOf {
// 只有条件变化时才会触发重组
listState.firstVisibleItemIndex > 0
}
}
...
}
现在就仅有 listState.firstVisibleItemIndex > 0变化时才会重组了。
三、Compose重组优化工具:布局检察器、编译器报告
3.1 Layout Inspector
在第2小节的一些示例中,我们使用了Layout Inspector的重组计数功能来量化优化效果。
以下是它的详细操作步骤:
3.1.1 打开Layout Inspector:运行项目到模拟器,点击下图中的图标

可能会报错:AS Layout Inspector报错:Could not download androidx.compose.ui;ui-android:1.6.6 from maven.google.com, Check the internet connection For offline repositories (not common) please specify -Dappinspection.use.dev,ar=true as a custom VM property.
解决办法: zhuanlan.zhihu.com/p/661454651
3.1.2 查看Layout Inspector界面,下图红框部分

3.1.3 点击app中的组件,查看Layout Inspector页面变化

图中1,2,3含义
- 1. Compositions (组合次数) :此可组合项进入组合阶段的总次数。这包括了初始组合和所有后续的重组。
- 2. Recomposing Children (重组子项次数) :此可组合项的直接子项进入组合阶段的总次数。这是一个累计值,有助于了解变更的影响范围。
- 3. Skipped (跳过次数) :此可组合项在可能发生重组时,被成功跳过组合阶段的次数。这个数值越高,通常意味着性能优化得越好。
它适合用于对比优化前后的差异,可以直观验证策略有效性。
3.2 Compose 编译器报告
3.2.1 集成
在每个模块的 build.gradle
文件中添加以下内容
kotlin
android { ... }
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
这是在 AGP 8.0 及更高版本中的配置方式。如果您使用的是较早版本的 Android Gradle 插件,可能需要使用
reportsDestination = file("$buildDir/compose_compiler")
的格式。
3.2.2 构建项目时会生成 Compose 编译器报告

reportsDestination 会输出三个文件。
<modulename>-classes.txt
: 关于本模块中类稳定性的报告。<modulename>-composables.txt
: 关于模块中可组合项的可重启和可跳过程度的报告。。<modulename>-composables.csv
: 可组合项报告的CSV
版本,您可以将其导入电子表格或使用脚本进行处理。<modulename>-module.json
: 编译阶段的优化指标
可组合项报告
composables.txt
文件中会详细说明给定模块的每个可组合函数的相关情况,包括其参数的稳定性以及它们是否可重启或可跳过
kotlin
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun RgbSelector(
stable color: Color
stable onColorClick: Function1<Color, Unit>
stable modifier: Modifier? = @static Companion
)
可以看到2.1.1小节用到的RgbSelector是可重启、可跳过,并且都是稳定参数。
类报告
文件 classes.txt
中包含关于给定模块中的类的类似报告。以下代码段是针对类 MainActivityViewModel
的输出:
kotlin
stable class MainActivityViewModel {
stable var color$delegate: MutableState<Color>
stable var colorLambda$delegate: MutableState<Color>
stable var colorRemember$delegate: MutableState<Color>
<runtime stability> = Stable
}
2.1.1小节用到的MainActivityViewModel也是稳定的,代码如下:
kotlin
class MainActivityViewModel : ViewModel() {
var color by mutableStateOf(Color.Red)
private set
fun changeColor(color: Color) {
this.color = color
}
}
优化指标
module.json是整个项目当前的一个概要
json
{
"skippableComposables": 17, // 可跳过重组的函数
"restartableComposables": 26,
"readonlyComposables": 0,
"totalComposables": 26,
"restartGroups": 26,
"totalGroups": 31,
"staticArguments": 35,
"certainArguments": 1,
"knownStableArguments": 346,
"knownUnstableArguments": 1, // 不稳定参数数量
"unknownStableArguments": 0,
"totalArguments": 347,
"markedStableClasses": 0,
"inferredStableClasses": 2, // 稳定类识别
"inferredUnstableClasses": 0,
"inferredUncertainClasses": 0,
"effectivelyStableClasses": 2,
"totalClasses": 2,
"memoizedLambdas": 20, // 记忆化Lambda
"singletonLambdas": 1,
"singletonComposableLambdas": 7,
"composableLambdas": 9,
"totalLambdas": 21 // 总的Lambda
}
需要着重关注的几个数据:
- knownUnstableArguments(不稳定参数数量)
- skippableComposables(可跳过重组的函数)
- restartGroups / totalGroups(重启作用域效率)
- memoizedLambdas(记忆化Lambda)
- inferredStableClasses(稳定类识别)
四、总结与延伸
本文我们深入探讨了Compose的核心------重组机制的优化策略。通过上述方法,我们能有效提升重组性能。需要注意的是,一个完整的性能优化体系还包含:
- 布局阶段优化: 例如使用
ConstraintLayout
减少测量次数,扁平化布局层级。 - 绘制阶段优化: 例如使用
drawWithCache
复用昂贵对象,避免在draw{}
块中频繁分配内存。 - 遵守阶段职责: 这是一条绝对的原则 :严禁在
@Composable
函数或布局修饰符中执行任何 I/O 操作、密集计算或分配大量临时对象,这些阻塞性操作会直接摧毁整个渲染管线的性能,与重组次数无关。
注:文中优化方式涉及的代码(优化前后)已重新整合到 Demo