前言

@Stable@Immuable 是 Compose 特有的类型稳定性注解,可以帮助 Compose 提升重组性能。本文将针对 Compose 类型的稳定性以及相关注解的使用做一个介绍。

1. 重组与稳定类型

我们知道 Compose 的重组非常“智能”,一个 Composable 函数在重组中被调用时,如果参数与上次调用时相比没有发生变化,则函数的执行会跳过重组,提升重组性能。但其实有时候即使参数没有发生变化重组也会进行,看下面的例子:

class MutableString(var data: String)@Composable
fun StableTest() {val str = remember { MutableString("Hello") }var state by remember { mutableStateOf(false) }if (state) {str.data = "World"}// WrapperText 会随 state 的变化而重组Button(onClick = { state = true }) {WrapperText(str)}
}@Composable
fun WrapperText(data: MutableString) {Text("${data.data}")
}

我们点击 Button 后 state 改变造成 StableTest 重组,MutableString 类型的 str 在重组前后指向同一实例,只是 data 值发生 “Hello” > “World” 的变化,如果在调用 WrapperText 时,对重组前后的参数进行比较将无法发现变化,但是实际执行会发现,重组并没有被跳过,此时 WrapperText 依然参与重组,正确地更新了文本。

重组中 Composable 参数进行比较的前提是参数类型必须是“稳定”类型,如果 Composable 参数中有不稳定类型,则 Composable 无法跳过重组。所以看来 MutableString 并非稳定类型,那什么样的类型算是“稳定”的呢?Compose 中稳定类型需符合以下特征:

  • 对于类型 T 的两个实例 ab,如果 a.equals.(b) 的结果是长期不变的,那么 T 是一个稳定类型。所以一个 Immutable 类型自然也是稳定类型
  • 如果类型 T 存在可变的 public 属性,且所有 public 属性的变化都能被感知并正确反映到 Compositioin,即属性的类型是 MutableState 的,那么 T 也是一个稳定类型。
  • 稳定类型的所有 public 的属性也必须是稳定类型。因为有可能你对 equals 进行了重写造成某个 public 属性不参与比较,但属性却有可能在 Composition 中被引用,为了保证引用的正确性,则要求它也必须是稳定的。

一言以蔽之,稳定类型要么不可变,要么其变化可被追踪。回看前面例子中的 MutableString,它的成员 data 不是 final 的且其变化无法被追踪,所以它并不是一个稳定类型。

2. @Stable 与 @Immutable

Compose 编译器在编译期会识别 Composable 函数的参数是否是稳定类型,当识别为稳定类型时,意味着参数比较的结果是可信的,此时会插入相关 equals 代码,以便于跳过不必要的重组。
编译器会将以下类型自动识别为稳定类型:

  • Kotlin 中的基本类型,Boolean, Int, Long, Float, Char 等等
  • String 类型
  • 各种函数类型、Lambda
  • 所有 public 属性都是 final (val 声明)的对象类型,且属性类型是不可变类型或可观察类型

不符合上述规范的类型是不稳定类型,但是我们可以通过手动添加 @Stable 或者 @Immutable 注解让编译器将其看待为稳定类型,@Immutable 代表类型完全不可变,@Stable 代表类型虽然可变但是变化可追踪。

文章开头的例子中,如果为 MutableString 添加 @Stable 或者 @Immutable 后,再次执行会发现结果中 “Hello” 不会变为 “World”。

//data 虽然是 var 但是由于添加 @Stable,被认为是稳定类型
@Stable class MutableString(var data: String)

MutableString 作为稳定类型被插入了 equals 逻辑,由于比较结果恒为 true 所以跳过重组。这造成了 str 的更新无法整成显示,不符合预期,因此我们添加 @Stable 注解时一定要慎之又慎,避免出现不符合预期的错误。

还有一点需要特别注意,对于 interface 添加 @Stable 注解后,其派生类默认都会被当做稳定类型处理。比如下面的 UiState 接口的子类都是稳定类型

@Stable
interface UiState<T : Result<T>> {val value: T?val exception: Throwable?val hasError: Booleanget() = exception != null
}

@Stable 与 @Immutable 在编译器的处理上并没什么不同,都是在适当的代码位置插入参数比较代码,而且 @Stable 相对于 @Immutable 的使用场景更广泛,除了修饰 Class,还可以修饰函数、属性等等,因此大家可以优先使用 @Stable,@Immutable 或许会在未来被逐渐废弃。

下面通过几个例子,再体会一下编译器对稳定类型的处理:

//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 注解
@Stable class StableMutableString(var data: String)
@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}")
}

以上除了 2 以外,其他 1,3,4,5 都都会被编译器作为稳定类型对待,字节码如下:

 // 1,3,4,5public static final void showString(String string, Composer $composer, int $changed) {//...Composer $composer = $composer.startRestartGroup(601350781);int $dirty = $changed;if ((i & 14) == 0) {// Composer#changed 对参数进行比较$dirty |= $composer.changed((Object) string) ? 4 : 2;}if ((($dirty & 11) ^ 2) != 0 || !$composer.getSkipping()) {// 参数输入有变化则调用 TextText($composer, string.getData(), /*...*/);} else {// 没有变化则不调用 Text$composer.skipToGroupEnd();}ScopeUpdateScope endRestartGroup = $composer.endRestartGroup();//...}// 2
public static final void showMutableString(MutableString string, Composer $composer, int $changed) {//...Composer $composer = $composer.startRestartGroup(1498293802);Text($composer, string.getData(), /*...*/);ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();//...}

可以看到,当编译器将参数类型识别为稳定类型时,会插入 $composer.changed((Object) string) 对参数与上次重组中的输入进行比较,看一下 changed 的实现非常简单:

override fun changed(value: Any?): Boolean {return if (nextSlot() != value) {updateValue(value)true} else {false}
}

如上,nextSlot() 从 Composition 中读取存储的上一次的参数与 value 进行比较,对 Slot 的概念不清楚的,可以看我的这篇文章:

需要注意,稳定类型的所有 public 子属性必须全部为稳定类型,对于上面例子中的 3 和 5,一旦有成员是非 fianl 或者非 MutableState 的,那么就不会被视为稳定类型了。

3. 提升类型的稳定性

通过前面介绍,我们知道了 Compose 编译器针对稳定类型的特殊处理,在日常开发中,我们可以留意那些可以被提升稳定性的类型,以提高重组性能。我们以官方 Sample 中的 JetSnack 源码为例,源码中有一个 · 数据类,定义如下:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */data class Snack(val id: Long,val name: String,val imageUrl: String,val price: Long,val tagline: String = "",val tags: Set<String> = emptySet()
)

Snack 所有成员均为 final,看起来是一个稳定类型,但是很遗憾它会被视为一个非稳定类型,因为 · 作为一个 interface 将被视为一个非稳定类型。因为编译器不知道其实现类是否是 Mutable 的,比如下面这样:

val set: Set<String> = mutableSetOf(“foo”)

实际上在 JetSnack 中,· 并不存在动态修改的场景,如果能让其被识别为稳定类型则可以提升重组性能。一个好消息是 Compose 编译器 1.2 之后,可以将 Kotlin 的 Immutable 集合(org.jetbrains.kotlin.kotlinx.collections.immutable)识别为稳定类型,例如 ImmutableSet,ImmutableList 等,即使他们只是 interface。因此我们可以通过修改 tags 的声明类型来提升其稳定性:

val tags: ImmutableSet<String> = persistentSetOf()

另一个提升稳定性的方法,就是通过添加本文介绍的 @Stable 注解,如下:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */@Stable
data class Snack(val id: Long,val name: String,val imageUrl: String,val price: Long,val tagline: String = "",val tags: Set<String> = emptySet()
)

看一下 JetSnack 中引用了 Snack 的类型的稳定性的变化

data class OrderLine(val snack: Snack,val count: Int
)data class SnackCollection(val id: Long,val name: String,val snacks: List<Snack>,val type: CollectionType = CollectionType.Normal
)@Composable
private fun HighlightedSnacks(index: Int,snacks: List<Snack>,onSnackClick: (Long) -> Unit,modifier: Modifier = Modifier
) { ... }
  • OrderLine 由于 snack 属性变为稳定类型,其自身会被自动推断为稳定类型。
  • SnackCollection 的 snacks 的 List 中的泛型类型是 Snack ,但是 List 本身是不稳定的,所以想要将 SnackCollection 改为稳定类型,可以添加 @Stable 或者 @Immutable,亦或者将 List 改为 ImmutableList
  • HighlightedSnacks 也是同样,可以通过添加 @Stable 注解,或者将 List 改为 ImmutableList 提升稳定性。注意 @Immutable 不能修饰函数。

前面讲过,我们对于 @Stable 和 @Immutable 注解的使用要慎之又慎,所以优先推荐使用不依靠注解提升稳定性的方法。

4. 跨 Modules 的类型引用

通常我们的项目中可能不止一个 Gradle Module 。很多项目会按照官方推荐的架构规范,将 UI 层、Data 层等分 Module 管理,Composable 定义在 UI 层,而数据类可能定义在 Data 层,被 UI 层引用。此时需要特别注意的是,Data 层的 Module 由于没有启动 Compose 编译器插件,对于非基本型的稳定性无法自动推断。比如前面 Snack 如果定义在单独的 Module 且没有启动编译期插件,那么即使将 List 改为 ImmutableList,对于使用到他的 UI 层 Composable 来说仍然无法识别为稳定类型。此时有以下几种方式解决:

  1. 添加 @Stable 或者 @Immutable 注解,强制设为稳定类型,这会导致增加对 compose-runtime 的依赖,注意没必要依赖 compose-ui 的任何库
  2. 为 Data 层的 Module 开启 Compose 插件
  3. 在 UI 层对 Data 层的类型进行封装,并添加稳定性注解。

当然,同样的问题也发生在对三方库的依赖上,而且三方库没法修改源码,只能用上面第三种方式予以解决。

5. 总结

  1. Compose 会针对稳定类型进行编译期优化,通过对输入参数的比较跳过不必要的重组
  2. 稳定类型包括所有的基本型、String类型、函数类型,以及符合以下条件的非基本类型:
  • 非 interface
  • 所有 public 属性均为 final
  • 所有 public 属性均为稳定类型或者 MutableState
  1. 通过添加 @Stable 或者 @Immutable 注解可以提升重组性能,注解的使用要慎重
  2. 跨 Module 引用数据类型时,需要通过辅助手段提升其稳定性

Compose 类型稳定性注解:@Stable @Immutable相关推荐

  1. java注解类型_Java注解类型

    本篇文章帮大家学习java注解类型,包含了Java注解类型使用方法.操作技巧.实例演示和注意事项,有一定的学习价值,大家可以用来参考. 标记注解类型 标记注解类型是没有元素的注解类型,甚至没有默认值. ...

  2. Hibernate3.X实现基于CLOB字段类型的注解方式:

    一:Hibernate3.X实现基于CLOB字段类型的注解方式的例子:下面直接上代码: 二:UserInfo.java package cn.gov.csrc.cms.model;import jav ...

  3. hibernate oracle clob 注解,Hibernate3.X实现基于CLOB字段类型的注解方式:

    一:Hibernate3.X实现基于CLOB字段类型的注解方式的例子:下面直接上代码: 二:UserInfo.java package cn.gov.csrc.cms.model; import ja ...

  4. PostgreSQL 30天 培训视频(SQL基础,备份恢复,HA,服务端编程,大数据,内核,应用案例)

    Postgres2015全国用户大会将于11月20至21日在北京丽亭华苑酒店召开.本次大会嘉宾阵容强大,国内顶级PostgreSQL数据库专家将悉数到场,并特邀欧洲.俄罗斯.日本.美国等国家和地区的数 ...

  5. postgresql最全整理资料,PostgreSQL 30天 培训视频(SQL基础,备份恢复,HA,服务端编程,大数据,内核,应用案例)

    转载自:http://blog.163.com/digoal@126/blog/static/16387704020141229159715/ 希望通过这些视频帮到一些朋友, 同时对视频中的错误点烦请 ...

  6. PostgreSQL Oracle 兼容性之 - PL/SQL DETERMINISTIC 与PG函数稳定性(immutable, stable, volatile)...

    标签 PostgreSQL , Oracle , 函数稳定性 , stable , immutable , volatile , DETERMINISTIC 背景 Oracle创建pl/sql函数时, ...

  7. ​Python 3 新特性:类型注解——类似注释吧,反正解释器又不做校验

    ​Python 3 新特性:类型注解 Crossin ​ 上海交通大学 计算机应用技术硕士 95 人赞同了该文章 前几天有同学问到,这个写法是什么意思: def add(x:int, y:int) - ...

  8. java 注解 属性 类型_跟光磊学Java开发-Java注解

    注解概述 注解(Annotation)相当于一种标记,在程序中加入注解就等于为程序打上某种标记以后,java编译器.开发工具或者其他的框架就可以通过反射来获取类以及类的成员上的注解,然后通过作相应的处 ...

  9. java注解字段类型相同_《java基础学习之——重复注解》

    在某些情况下,您要将相同的注释应用于声明或类型使用.从JavaSE 8版本开始,重复注释使您能够做到这一点. 例如,您正在编写代码以使用定时服务,使您能够在给定时间或某个时间表运行方法,类似于UNIX ...

最新文章

  1. 学习笔记:UITabBarController使用详解
  2. Dynamic AX ERP 4.0 数据导出(上)
  3. mysql 日志还原数据库_通过Mysql-bin日志恢复还原数据
  4. 重磅发布 | 承载亿级流量的开发框架,闲鱼Flutter技术解析与实战大公开
  5. Hibernate继承:每个类层次结构的表
  6. 统计学习方法笔记(李航)———第四章(朴素贝叶斯法)
  7. LINUX 文件合并,去重
  8. Smartisan OS ROM 小米手机 2/2S 标准版 刷机教程
  9. python字典的key提取_python 字典操作提取key,value
  10. java冒泡排序图解_[图解] 冒泡排序
  11. 易大师接口自动化测试平台如何创建不同协议的接口并进行测试
  12. Kafka创建topic报错:Error: Exception thrown by the agent : java.rmi.server.ExportException: Port already
  13. linux yum命令详解,yum命令详解
  14. 【docker】Mac m1 系统使用docker发布镜像
  15. 飘飘微课计算机百度云,数学微课_百度云资源_盘多多如风搜_盘搜搜_哎哟喂啊...
  16. 【C#编程】两点距离计算
  17. (侯捷C++)1.2面向对象高级编程(上)
  18. 2020年中国储能材料产业链上中下游及未来发展趋势分析,电化学储能高速发展,储氢瓶、加氢站建设拉动需求「图」
  19. 标题:信号、传输介质、数制转换
  20. 2021届腾讯实习笔试题

热门文章

  1. 深入理解计算机系统CSAPP复习
  2. Openlayers设置ESPG900913作为影射算法
  3. Android开发:设置背景图片
  4. 解决字符串存在\u6d4b\u8bd5\u5206\u7c7等中文汉字(乱码)(unicode码)
  5. form表单中id与name的区别
  6. GMSK技术的原理(Principle of GMSK technologies)
  7. WaitForSingleObject -- setevent 讲解与编程示例
  8. VBA word自动排版(10)-结合SQL数据库批量替换WORD文本
  9. Python 3 怎么快速搭建服务器
  10. 原生Js通过class属性值获取对象