资讯动态

Compose 重组优化 - 参数稳定性(跳过不必要重组)

发布时间:2026/9/5 23:44:37 来源:尧图企业网站定制
参考文章1参考文章2一、概念1.1 稳定类型的定义这个值的变化可被 Compose 观察不会在 Compose 不知道的情况下变化。不可变对于类型 T 的两个实例 a 和 b如果 a.equals.(b) 的结果是长期不变的那么 T 是一个稳定类型。因此不可变类型Immutable也是稳定型。变化可被观察如果类型 T 存在 public 的 var 属性能被外部访问并更改值且所有 public 属性的变化都能被感知并正确反映到 Compositioin即属性的类型是 MutableState 的那么 T 是一个稳定类型。就是把该class中var声明的public属性都用 mutableStateOf() 包装以满足可观察性稳定类型的所有 public 属性也必须是稳定类型。因为有可能你对 equals() 进行了重写造成某个 public 属性不参与比较但属性却有可能在 Composition 中被引用为了保证引用的正确性则要求它也必须是稳定的。1.2 自动识别的稳定类型基本类型Int、Long、Float、Double、Boolean、Char、String 类型、函数类型Lambda。所有 public 属性都是 final val 声明且类型是不可变类型或可观察类型。1.3 手动注解成稳定类型 Stable Immutable一般不需要手动处理编译器会自动推断稳定性仅对「编译器误判为不稳定的类型」时才手动加。仅仅代表约定注解一个类会覆盖编译器对该类的推断比较结果恒定为 true 不会重组使用不当修饰了不稳定类型会造成无法更新显示。修饰 interface 派生的子类都会被当做稳定类型。Stable代表类型虽然可变但是变化可被观察该类型是可变的但如果任何public属性或方法会产生与先前调用不同的结果Compose 运行时将收到通知虽然对象内部的数值会发生变化但是这种变化可以被Compose识别如重写 equals。Immutable代表类型完全不可变该类型中任何属性的值在构造对象后都不会改变所有属性val并且所有方法都是引用透明的。也许会被废弃优先使用Stable除了修饰类还能修饰属性和函数使用场景更广泛//1. 不可变类型String是稳定型。 Composable fun showString(string: String) { Text(text Hello ${string}) } ​ //2. 可变类型有可变的属性不是稳定型。 class MutableString(var data: String) Composable fun showMutableString(string: MutableString) { Text(text Hello ${string.data}) } ​ //3. 不可变类型成员属性全是 final是稳定型。 class ImmutableString(val data: String) Composable fun showImmutableString(string: ImmutableString) { Text(text Hello ${string.data}) } ​ //4. 可变类型加Stable注解是稳定型。 //data 虽然是 var 但是由于添加 Stable被认为是稳定类型。 //由于比较结果恒为 true 会跳过重组造成了 data 内容的更新无法显示出来。 //需要重写 equals() Stable class StableMutableString(var data: String) { override fun equals(other: Any?): Boolean { if (this other) return true if (javaClass ! other?.javaClass) return false other as StableMutableString return data other.data } } Composable fun showStableMutableString(string: StableMutableString) { Text(text Hello ${string.data}) } ​ //5. 可变类型变化可被追踪是稳定型。 class MutableString2(val data: MutableStateString mutableStateOf()) Composable fun showMutableString2(string: MutableString2) { Text(text Hello ${string.data}) }1.4 使用不可变集合接口会被视为不稳定类型如ListTCompose编译器在处理时虽然看到了 val 声明不可重新赋值但不知道它的实现类是可变通过mutableListOf()创建还是不可变通过listOf()创建的。由于其内部数据是否可变无法保证便将其视为不稳定每次都会重组。被 Immutable 修饰的 data class 有一个 val 属性是 List 类型虽然注解强调该类型是不可变的但内部属性还是可变的。Compose编译器1.2版本后可将 Kotlinx 的 Immutable 集合识别为稳定类型即便它们是接口。最新版本implementation(org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0)//看似成员都是 final但接口被认为是不稳定性 data class Demo( val id: Long, val name: String. val tag: ListString //接口可传入 mutableListOf() 或 listOf() )mutableListOf(A,B,C).toImmutableList() Composable fun Demo( tag: ImmutableListString )1.5 跨 Module 的情况通常项目中可能不止一个模块Compose定义在 UI 模块数据类定义在 Data 模块如果 Data 模块没有启动 Compose 插件对于非自动识别的稳定类型会无法推断例如手动将数据类中的 List 改为 ImmutableList 也无法识别。同样的问题也发生在无法修改源码的三方库依赖上。添加 Stable 或者 Immutable 注解强制设为稳定类型这会导致增加对 compose-runtime 的依赖注意没必要依赖 compose-ui 的任何库。为 Data 层的 Module 开启 Compose 插件。在 UI 层对 Data 层的类型进行封装并添加稳定性注解。//第三方库中的类型 class Person( val name: String ) //稳定的包装类 Stable class DataWrapperT( val value: T ) Composable fun Out() { //创建第三方库中的对象并赋值给包装类 val dataWrapper DataWrapper(Person()) In(dataWrapper) } Composable fun In( dataWrapper: DataWrapperPerson ) { Text(dataWrapper.value.name) }二、强制跳过模式Strong Skipping Mode参考文章不稳定类型无法自动进行跟踪和比较无论内容较上次有没有变化强制重组以确保渲染的正确功能正确性比性能更重要。但在编码时去更改不稳定类型的状态可能只是少数情况为了跳过这不必要的重组我们需要付出更多的成本弄懂稳定类型、评估Stable的添加、对跨Module或第三方库进行适配。自 Compose v1.5.4 引入强制跳过模式Strong Skipping Mode放宽了跳过重组的限制让参数中的不稳定类型也能参与比较。稳定类型依旧使用 Object.equals() 比较不稳定类型与 Composition 存储的历史值做 比较。Lambda 如果捕获了不稳定类型依然可以被记忆。相当于弱化了不稳定类型和稳定类型的区别不稳定类型使用 比较这里会存在风险那些容器对象。如果我们想要使用值比较替代地址比较还是要依靠 Stable 。对于那些在第三方库中无法添加注解的类型可以使用稳定性配置文件(stability configuration files)详见。2.1 开启方式Kotlin 2.0.2 之后强跳模式会默认开启。composeCompiler { enableStrongSkippingMode true }2.2 带来的副作用上面说去更改不稳定类型的状态是少数情况但还是有的。例如使用了可变集合 MutableList。当点击 Button 时 list 的内容发生变化但由于不稳定类型采用 比较的是引用集合实例没变此时会返回 true 使得接收该参数的可组合项跳过了重组容器内元素的变化无法得到正确刷新。使用 mutableStateListOf() 创建可观察的 List或者重新赋值。另一方面不稳定类型通过 作比较后返回 false 也不一定值不相等。通常情况下我们不会对 mutable 类型使用拷贝构造出多个相同值的实例因此大多数情况下实例不同即代表值不同即便错杀了一两个无辜的 case 也无伤大雅。2.3 对于 Lambda 传参Lambda 默认是稳定类型但捕获了外部不稳定类型如ViewModel的 Lambda 会被视为不稳定类型。不再是单纯的函数类型而是函数外部状态。不捕获任何外部变量会为 Lambda 创建一个静态单例。捕获了外部稳定类型Compose 编译器会自动为其包装一个 remember 避免 Lambda 因重新创建实例而在重组中 diff 失败。这很重要试想一下很多组件都有 Lambda 回调例如 onClick 等如果 Lambda 不被记忆意味着大部分都无法跳过重组。捕获了不稳定类型强跳模式开启下不稳定类型的 Lambda 一样都会被包装 remember捕获的外部变量 ViewModel 变为 remember 的 key。 Key 采用同样的比较规则即不稳定类型 地址比较稳定类型 Object.equals() 值比较。//之前 onClick { viewModel.onClicked() } //强跳 onClick remember(viewModel){ viewModel.onClicked() }

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价