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 必须指定 key :items(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。

相关推荐
tongyiixiaohuang4 小时前
WMS已发货状态反写金蝶云星空:单策略实战教程
android
好的佩奇4 小时前
Dart 之类修饰符
android·flutter·dart
alexhilton6 小时前
用Konsist与Lint强制执行设计系统契约
android·kotlin·android jetpack
唐青枫8 小时前
Kotlin Delegation 委托详解
kotlin
北京自在科技10 小时前
谷歌安卓 Gemini 迎来界面改版,Find Hub 接入 Gemini 支持物品记忆查找
android·安卓
YB137512 小时前
Jetpack Compose 动画Modifier.animateItem()
android
2401_8858850413 小时前
巴林语音通知接口如何对接?系统告警语音API接入教程
android·前端·网络·python·语音识别
收纳过往13 小时前
数据库加索引的时机:慢查询日志说话
android·开发语言·数据库·python·低代码·rxjava
狂热开发者14 小时前
车载中控要给鸿蒙用户,能不能和 Android 共用一个二维码?
android·华为·harmonyos
.Bryce.15 小时前
T2-LLM评测不等于Agent评测
android·java·人工智能·后端·gpt·agent·1024程序员节