Jetpack Compose 稳定性、重组优化(Stable / @NonRestartableComposable)

Jetpack Compose 稳定性、重组优化(Stable / @NonRestartableComposable)

重组(Recomposition)是 Compose 更新 UI 的机制;稳定性 (Stable) 决定组件能不能被跳过重组;@NonRestartableComposable 修改可重启 / 可跳过行为,属于微优化,不要滥用

一、什么是重组 Recomposition

  • @Composable 只是普通 Kotlin 函数,状态变化时,Compose 会重新执行受影响的 @Composable 函数,生成新 UI 树,这个过程就是重组。
  • Compose 目标:只重组真正变化的部分,尽可能跳过不需要更新的组件
  • 两个关键能力(编译器生成):
  1. Restartable(可重启) :拥有独立重组作用域,读取内部 State 时,可以局部单独触发重组,不需要父组件参与。默认所有 @Composable 都是可重启CSDN博...。
  2. Skippable(可跳过) :当全部入参都是稳定类型,并且参数值没有变化,父组件重组时,可以直接跳过本函数执行,不跑函数体、不重建子 UI。

✅ 可重启 ≠ 可跳过;可跳过必须满足:所有参数 Stable。

二、稳定类型 @Stable / @Immutable

1. 什么是稳定类型

满足契约

解决稳定性问题 | Jetpack Compose | Android Developers - 安卓文档

  1. 对象相等 equals() 返回 true,对象内容就不会偷偷变化;
  2. 外部修改对象属性,Compose 可以感知(如 MutableState);
  3. 所有公开属性也必须是稳定类型。

Compose 编译器自动判定稳定:

  • 基础类型:Int、String、Boolean、Float、Dp、Color
  • Lambda 函数(无捕获可变状态时默认稳定)
  • 全部属性是val且都是稳定类型的data class(编译器自动推断)
  • @Stable@Immutable注解的类CSDN博...

⚠️ 高频不稳定陷阱

  1. List<T> / MutableList / Set / Map 标准库接口 → 不稳定,编译器无法确定运行时是否可变。即使你传的是不可变集合,接口本身不稳定,整个类直接标记 unstable。
  2. 类里面有var可变字段 → 不稳定。
  3. 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

  1. 没有独立重启域:内部读取 State,不会在本函数触发重组,重组会向上冒泡,跑到最近一层 Restartable 父组件执行;
  2. 不可跳过:父组件重组,它一定跟着重组,无论参数是否不变;
  3. 好处:节省编译器生成 scope 的微小开销;
  4. ⚠️仅适合极简包装函数,严禁业务页面、复杂组件使用

2. 适用场景

  • 极简单包装组件,只调用 1 个子 Composable,无复杂逻辑,不会自己读取任何 State
  • 仅做参数转发、简单封装,不需要独立重组域
kotlin 复制代码
// ✅适合:极简包装,无状态读取
@Composable
@NonRestartableComposable
fun MySpacer(height:Dp){
    Spacer(modifier = Modifier.height(height))
}

3. 不要用的场景

  1. 函数内部读取remember / mutableStateOf / ViewModel.state
  2. 函数内部有多分支逻辑、大量子组件;
  3. 业务 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 重组优化完整最佳实践

  1. 数据类稳定性优先
  • 实体类尽量全部val;杜绝var
  • 集合优先使用kotlinx‑collections‑immutable不可变集合;
  • 第三方 Java 实体,手动加@Stable告诉编译器契约;
  1. Lambda 稳定性陷阱
kotlin 复制代码
// ❌每次重组都会生成新lambda对象,如果参数不稳定,子组件会频繁重组
@Composable fun Parent(){
   Child(onClick = { /*新lambda实例*/ })
}
// ✅使用remember保存lambda,稳定引用
@Composable fun Parent(){
   val click = remember {{}}
   Child(onClick = click)
}
  1. 状态提升 + 拆分组件:把读取 State 逻辑收缩到最小组件,缩小重组范围;不要把所有状态都放在根组件。

  2. LazyColumn 必须指定 keyitems(list,key={it.id}),避免列表滑动、增删时批量重组,防止 item 内部状态错乱CSDN博...。

  3. remember 缓存重计算 :重组会反复执行 @Composable,耗时计算、对象创建,使用remember(key){...}缓存结果。

  4. 不要盲目使用 @NonRestartableComposable:只有极简包装组件才考虑,业务 UI 禁止使用。

  5. 查看编译器稳定性报告 在 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 函数**,修改编译器生成的重组逻辑:

  1. ❌ 移除本函数独立 Restart 重组域
  2. ❌ 直接丧失 Skippable 可跳过能力(不管参数是不是 Stable)
  3. 如果函数内部读取 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 无关

七、常见误区总结

  1. ❌ 误区:对象引用不变,就不会重组。

错,如果类型标记 unstable,就算同一个对象,父重组依然会执行。Stable 类型是跳过重组的前提条件

  1. ❌ 误区:@NonRestartableComposable 是性能优化神器,到处加。

错,它是微优化,代价是丢失局部重组能力,滥用会扩大重组范围。

  1. ❌ 误区:data class 自动就是 stable。

错,如果里面有 List、var 字段,会被判定 unstable。

相关推荐
__Witheart__33 分钟前
3588 Android 13 预装apk失败 —— 不再使用apps.mk
android
__Witheart__42 分钟前
3588 Android 串口软件提示“没有串口读写权限”
android
搭贝2 小时前
国资报送责任制怎么建?三级责任矩阵设计
android·数据库·人工智能·线性代数·低代码·矩阵·制造
hunterandroid2 小时前
[鸿蒙从零到一] ArkUI 声明式渲染管线深度解析:Diff、复用与局部刷新
android
JMchen1233 小时前
Jetpack 内核实战(十):实战(下)——编辑页、全局设置与内存泄漏排查
kotlin·内存泄漏·android 实战·datastore 设置·jetpack 项目优化·编辑页开发
林栩link3 小时前
【车载 Android】从 AAR 到 Plugin:Launcher 卡片插件化解耦实践
android
小小测试开发4 小时前
RAG应用评测:从指标体系到LLM-as-a-Judge的自动化落地
android·运维·人工智能·自动化
paopao_djshddhdj4 小时前
钉钉与钉钉服务商有什么区别?为什么建议选择服务商
android·钉钉
xcLeigh4 小时前
Go入门:rune与byte的区别和使用场景
android·javascript·golang