Jetpack Compose 稳定性、重组优化(Stable / @NonRestartableComposable)
重组(Recomposition)是 Compose 更新 UI 的机制;稳定性 (Stable) 决定组件能不能被跳过重组;@NonRestartableComposable 修改可重启 / 可跳过行为,属于微优化,不要滥用。
一、什么是重组 Recomposition
- @Composable 只是普通 Kotlin 函数,状态变化时,Compose 会重新执行受影响的 @Composable 函数,生成新 UI 树,这个过程就是重组。
- Compose 目标:只重组真正变化的部分,尽可能跳过不需要更新的组件。
- 两个关键能力(编译器生成):
- Restartable(可重启) :拥有独立重组作用域,读取内部 State 时,可以局部单独触发重组,不需要父组件参与。默认所有 @Composable 都是可重启CSDN博...。
- Skippable(可跳过) :当全部入参都是稳定类型,并且参数值没有变化,父组件重组时,可以直接跳过本函数执行,不跑函数体、不重建子 UI。
✅ 可重启 ≠ 可跳过;可跳过必须满足:所有参数 Stable。
二、稳定类型 @Stable / @Immutable
1. 什么是稳定类型
满足契约
解决稳定性问题 | Jetpack Compose | Android Developers - 安卓文档
- 对象相等
equals()返回 true,对象内容就不会偷偷变化; - 外部修改对象属性,Compose 可以感知(如 MutableState);
- 所有公开属性也必须是稳定类型。
Compose 编译器自动判定稳定:
- 基础类型:
Int、String、Boolean、Float、Dp、Color - Lambda 函数(无捕获可变状态时默认稳定)
- 全部属性是
val且都是稳定类型的data class(编译器自动推断) - 被
@Stable、@Immutable注解的类CSDN博...
⚠️ 高频不稳定陷阱
List<T> / MutableList / Set / Map标准库接口 → 不稳定,编译器无法确定运行时是否可变。即使你传的是不可变集合,接口本身不稳定,整个类直接标记 unstable。- 类里面有
var可变字段 → 不稳定。 - Java 类、第三方 SDK 实体类 → 默认不稳定。
kotlin
// ❌ 不稳定:List接口本身不稳定,即使val
data class User(val id:Int,val name:String,val tags:List<String>)
// ✅方案1:使用不可变集合库 kotlinx‑collections‑immutable
data class User(val id:Int,val name:String,val tags:ImmutableList<String>)
// ✅方案2:手动@Stable注解,告诉编译器这个类型遵守稳定契约
@Stable
data class User(val id:Int,val name:String,val tags:List<String>)
@Immutable:代表类实例完全不可变,所有属性 val,初始化后完全不能改 ;@Stable:允许内部可变,但变化必须通过 MutableState,Compose 可以感知变化。
2. 不稳定类型带来什么后果
只要函数任意一个参数是不稳定类型 :
👉 该组件失去 Skippable 可跳过能力;父组件一旦重组,无论对象内容有没有变,本组件一定会跟着重组,造成大量不必要执行、性能损耗Andro...。
注意:引用相等 ≠ Stable;就算传入同一个对象实例,只要类型标记 unstable,依旧不能跳过重组。
@Stable不会修改对象本身,只是契约注解,你代码依然要保证对象不可变 / 状态用 MutableState。
三、@NonRestartableComposable
1. 作用
移除该 @Composable 的独立 Restart 重组作用域,同时失去 Skippable 跳过能力稀土掘金。
普通 @Composable (默认 Restartable):
- 拥有自己重组 scope;内部读取 State,可以自己单独触发重组,父组件不受影响;参数稳定不变时可以被跳过。
@NonRestartableComposable:
- 没有独立重启域:内部读取 State,不会在本函数触发重组,重组会向上冒泡,跑到最近一层 Restartable 父组件执行;
- 不可跳过:父组件重组,它一定跟着重组,无论参数是否不变;
- 好处:节省编译器生成 scope 的微小开销;
- ⚠️仅适合极简包装函数,严禁业务页面、复杂组件使用。
2. 适用场景
- 极简单包装组件,只调用 1 个子 Composable,无复杂逻辑,不会自己读取任何 State
- 仅做参数转发、简单封装,不需要独立重组域
kotlin
// ✅适合:极简包装,无状态读取
@Composable
@NonRestartableComposable
fun MySpacer(height:Dp){
Spacer(modifier = Modifier.height(height))
}
3. 不要用的场景
- 函数内部读取
remember / mutableStateOf / ViewModel.state; - 函数内部有多分支逻辑、大量子组件;
- 业务 Item、页面组件;
如果加上注解,状态变化会向上冒泡,导致上层大范围不必要重组,性能反而变差。
补充:@NonSkippableComposable
依旧保留 Restartable 独立重组域;但是永远不可跳过,父重组一定执行,自身读取 state 依旧可以局部重启。极少使用。
| 注解 | 可 Restart 独立重组 | 可 Skippable 跳过重组 | 说明 |
|---|---|---|---|
| 默认 @Composable | ✅ | ✅(参数全 stable) | 标准,业务绝大多数场景 |
| @NonRestartableComposable | ❌ | ❌ | 去掉独立 scope,极简 wrapper 微优化 |
| @NonSkippableComposable | ✅ | ❌ | 可以局部重启,但父重组必执行 |
四、稳定 / 不稳定对比演示
kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Text
import androidx.compose.foundation.layout.button
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.runtime.NonRestartableComposable
// 不稳定数据类:包含List接口
data class UnStableItem(val id:Int,val name:String,val tags:List<String>)
// 稳定数据类
@Stable
data class StableItem(val id:Int,val name:String,val tags:List<String>)
@Composable
fun StabilityDemo(){
var count by remember { mutableIntStateOf(0) }
Column {
Button(onClick = { count++ }) {
Text("点击:$count")
}
// 稳定对象,参数不变时,count变化不会触发重组
val stableData = remember { StableItem(1,"稳定数据", listOf("a","b")) }
StableCard(stableData)
// 不稳定对象,父一重组,这里必重组
val unStableData = remember { UnStableItem(2,"不稳定数据", listOf("x")) }
UnStableCard(unStableData)
}
}
@Composable
fun StableCard(item:StableItem){
Text("StableCard:${item.name}")
}
@Composable
fun UnStableCard(item:UnStableItem){
Text("UnStableCard:${item.name}")
}
// 示例 NonRestartableComposable:仅简单包装
@Composable
@NonRestartableComposable
fun SimpleWrapper(content:@Composable ()->Unit){
content()
}
五、生产环境 Compose 重组优化完整最佳实践
- 数据类稳定性优先
- 实体类尽量全部
val;杜绝var; - 集合优先使用
kotlinx‑collections‑immutable不可变集合; - 第三方 Java 实体,手动加
@Stable告诉编译器契约;
- Lambda 稳定性陷阱
kotlin
// ❌每次重组都会生成新lambda对象,如果参数不稳定,子组件会频繁重组
@Composable fun Parent(){
Child(onClick = { /*新lambda实例*/ })
}
// ✅使用remember保存lambda,稳定引用
@Composable fun Parent(){
val click = remember {{}}
Child(onClick = click)
}
-
状态提升 + 拆分组件:把读取 State 逻辑收缩到最小组件,缩小重组范围;不要把所有状态都放在根组件。
-
LazyColumn 必须指定 key :
items(list,key={it.id}),避免列表滑动、增删时批量重组,防止 item 内部状态错乱CSDN博...。 -
remember 缓存重计算 :重组会反复执行 @Composable,耗时计算、对象创建,使用
remember(key){...}缓存结果。 -
不要盲目使用 @NonRestartableComposable:只有极简包装组件才考虑,业务 UI 禁止使用。
-
查看编译器稳定性报告 在 build.gradle 配置 compose 编译器报告,输出 html 报告,查看哪些类是 unstable,定位重组元凶。
kotlin
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
}
Android Studio 插件Compose Stability Analyzer可以直接编辑器查看稳定 / 不稳定标记
六、@Stable vs @NonRestartableComposable 核心区别
@Stable作用于【数据类型】,控制「能不能跳过重组 (Skippable)」;@NonRestartableComposable作用于【@Composable 函数】,控制「有没有独立重组域 (Restart)」
| 维度 | @Stable | @NonRestartableComposable |
|---|---|---|
| 修饰目标 | 类 / 接口(数据模型) | @Composable 函数 |
| 作用对象 | 数据、对象、入参类型 | UI 函数本身、重组作用域 |
| 影响能力 | 控制 Skippable(是否可跳过) | 控制 Restart(是否可独立重启) |
| 是否改变函数逻辑 | 不改变运行逻辑,给 Compose 编译器契约提示 | 修改 Compose 编译器生成的重组代码 |
| 核心效果 | 参数全部 stable,父重组时本组件可以被跳过 | 移除独立重组 scope;内部读 State 不会在本层触发重组,向上冒泡;同时丧失 skippable 能力 |
| 使用场景 | 修复数据类不稳定,解决无辜重组 | 仅极简包装函数,做微小编译层优化 |
1. @Stable(数据层面)
@Stable 告诉 Compose 编译器:这个类遵守稳定契约:equals 相等则对外表现不变,变化通过 MutableState 通知。
- 只管数据类型,不管函数。
- 如果一个 Composable 所有入参都是
@Stable类型 → 该函数获得**Skippable(可跳过)**能力。 - 父组件重组时,如果参数引用没有变化,这个 Composable 直接跳过不执行。
- 如果类型是 Unstable,无论对象引用有没有复用,都不能跳过,父重组就跟着跑。
kotlin
// 修饰数据类,告诉编译器这是稳定类型
@Stable
data class User(val id: Int, val name: String, val tags: List<String>)
@Composable
fun ShowUser(user: User) {
// user是Stable类型,父重组时,user不变就可以跳过本函数
}
2. @NonRestartableComposable(函数层面)
修饰**@Composable 函数**,修改编译器生成的重组逻辑:
- ❌ 移除本函数独立 Restart 重组域
- ❌ 直接丧失 Skippable 可跳过能力(不管参数是不是 Stable)
- 如果函数内部读取 State,不会在当前函数触发重组,重组向上冒泡到上层最近的 Restartable Composable
kotlin
@Composable
@NonRestartableComposable
fun SimpleWrapper(content: @Composable () -> Unit) {
content()
}
重点:
哪怕这个函数所有参数全是 @Stable ,加了
@NonRestartableComposable之后,依然不可跳过。父组件重组,这个函数一定执行。
3. 两者组合后的行为演示
// ✅稳定数据
@Stable
data class Msg(val text: String)
// 场景1:普通默认 @Composable
// 参数stable → ✅可重启、✅可跳过
@Composable
fun NormalCard(msg: Msg) { /* */ }
// 场景2:加上 @NonRestartableComposable
// 虽然msg是@Stable稳定类型,但是:❌不可重启、❌不可跳过
@Composable
@NonRestartableComposable
fun NoRestartCard(msg: Msg) { /* */ }
关键点:
@NonRestartableComposable会直接废掉 Skippable,跟参数是否 @Stable 无关。
七、常见误区总结
- ❌ 误区:对象引用不变,就不会重组。
错,如果类型标记 unstable,就算同一个对象,父重组依然会执行。Stable 类型是跳过重组的前提条件。
- ❌ 误区:@NonRestartableComposable 是性能优化神器,到处加。
错,它是微优化,代价是丢失局部重组能力,滥用会扩大重组范围。
- ❌ 误区:data class 自动就是 stable。
错,如果里面有 List、var 字段,会被判定 unstable。