参考文章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: MutableState<String> = mutableStateOf("")) @Composable fun showMutableString2(string: MutableString2) { Text(text = "Hello ${string.data}") }1.4 使用不可变集合
接口会被视为不稳定类型(如List<T>),Compose编译器在处理时虽然看到了 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: List<String> //接口,可传入 mutableListOf() 或 listOf() )mutableListOf("A","B","C").toImmutableList() @Composable fun Demo( tag: ImmutableList<String> )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 DataWrapper<T>( val value: T ) @Composable fun Out() { //创建第三方库中的对象并赋值给包装类 val dataWrapper = DataWrapper(Person("")) In(dataWrapper) } @Composable fun In( dataWrapper: DataWrapper<Person> ) { 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() }