1、重组优化的核心思想
- 定义:状态变化时,让尽可能少的可组合函数以尽可能快的速度执行完成。
- 关键词:尽可能少、尽可能快
2、常见重组优化
其实在前面介绍Compose的时候,我们也多少提到过一些重组优化,这里主要是将前面提到过的重组优化、实际开发中常见错误怎么重组优化以及Compose中还提供了哪些重组优化的API做一次汇总,帮助我们刚上手时"避坑"。
2.1 控制重组范围:
让状态变化只影响"必要区域"
2.1.1 拆分复杂的可组合函数: 避免"牵一发而动全身"
- 优化前:因name对象未被缓存,每次重组后都会创建新的对象,进而导致名称Text()在每次点击后都会重组。
点击操作 -> age累加 -> Test()重组 -> 重新创建name -> name Text()重组。
kotlin
@Composable
fun Test(){
val name = "Hello world!!"
//可观察状态
var age by remember { mutableIntStateOf(18) }
Column {
//名称
Text(text = name)
//年龄
Text(
modifier = Modifier.clickable(onClick = { //点击后累加
age++
} ),
text = "$age",)
}
}
- 优化后:点击操作后,与age依赖的函数只有AgeTest(),Test()和NameTest()函数不受其影响。
kotlin
@Composable
fun Test(){
Column {
//名称
NameTest()
//年龄
AgeTest()
}
}
@Composable
fun NameTest() {
Text(text = "Hello world!!")
}
@Composable
fun AgeTest() {
//可观察状态
var age by remember { mutableIntStateOf(18) }
Text(
modifier = Modifier.clickable(onClick = { //点击后累加
age++
} ),
text = "$age",)
}
2.1.2 列表用key控制重组颗粒度:避免"批量无效重组"
在使用LazyColumn/LazyRow 未指定 key 时,默认用 "列表索引" 作为标识,列表增删 / 排序时会导致大量无关项重组。
如果我们没有指定key,那么默认key就是index,假如我们删除第一项(index =0),会导致后续所有的索性变更(即都会左移:2->1,1->0),从而导致全部重组。--此时后面item无内容变化
指定key后,Compose识别后面item无内容变化,不会重组。--重组数量从"N -> 1"
ini
@Composable
fun ProductList(products: List<Product>) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
// 指定 key 为 product.id:唯一标识每个列表项
items(
items = products,
key = { product -> product.id } // 核心优化:用唯一 ID 替代索引
) { product ->
ShopItem(
product = product,
isFavorite = false,
onFavoriteClick = {}
)
Divider()
}
}
}
2.2 避免无效重组: 让"不变的状态"不发生重组
在Compose 简介和基础使用1 简介 1.1 背景 2019 年 5 月(首次亮相)在 Google I/O 大会上, - 掘金 中2.4.5.2 保存界面状态方式章节中提到过
特性 | remember | rememberSaveable |
---|---|---|
重组时是否保留 | 是(核心功能) | 是(继承 remember 的能力) |
重建时是否保留 | 否(状态随组件实例销毁) | 是(通过 Bundle 持久化) |
适用数据类型 | 任意类型 | 基本类型、可序列化类型(或需自定义保存逻辑) |
性能开销 | 低(内存级保存) | 略高(涉及 Bundle 读写) |
典型使用场景 | 临时状态(如列表展开 / 折叠) | 需持久化的用户输入(如表单、设置) |
2.2.1 remember
remember 是Compose API提供的缓存接口,避免每次重组时重新创建对象或者重新计算。 如下,"val showName = "Hello world!!--$name""写法上面分析过,每次点击后name Text()都会发生重组。通过remember 缓存,那么只有name发生变化时name Text()才会重组。
kotlin
@Composable
fun Test(name: String){
//状态
// val showName = "Hello world!!--$name"
//remember 普通缓存
val showName by remember(name) { mutableStateOf("Hello world!!--$name") }
var age by remember { mutableIntStateOf(18) }
// rememberSaveable 跨配置状态缓存
val showName by rememberSaveable(name) { mutableStateOf("Hello world!!--$name") }
var age by rememberSaveable { mutableIntStateOf(18) }
Column {
//名称
Text(text = showName)
//年龄
Text(
modifier = Modifier.clickable(onClick = { //点击后累加
age++
} ),
text = "$age",)
}
}
2.2.2 rememberSaveable
rememberSaveable 是Compose API提供的缓存接口,当状态需要在配置变更(如屏幕旋转、语言切换)后保留时,使用 rememberSaveable 可以实现跨配置的状态缓存,避免状态丢失和不必要的重新计算。
如上示例假设showName、age需要在屏幕旋转、语言切换后保留之前状态,那么就可以用rememberSaveable 缓存。
2.2.3 rememberUpdatedState
副作用生命周期大于状态的变化周期(例如副作用中延迟、循环等),且副作用中需要获取最新的状态值。 分析:- LaunchedEffect(Unit)副作用中使用Unit表示没监听任何状态,所以只在首次重组时创建启动协程,后续重组不会再重新创建新的启动协程,并且旧的协程也不会被打断。
- reportMessage 是可观察状态,内部直接通过副作用使用时,协程捕获到的是这个状态的引用,所以修改后内部延迟也能打印最新的值。而通过参数传递时传递的是具体的值(String),所以不使用rememberUpdatedState只能打印旧值,使用后rememberUpdatedState可以监听值的变化,保证副作用中打印的是最新的值。
kotlin
@Composable
fun ReportMessageScreen() {
// 父组件管理的消息状态,可动态更新
var reportMessage by remember { mutableStateOf("初始消息") }
// 子组件:负责延迟上报消息
MessageReporter(currentMessage = reportMessage)
LaunchedEffect(Unit) {
delay(5000) // 延迟3秒上报
// 问题:即使currentMessage已更新,仍会上报初始值
Log.d("Report", "内部上报: $reportMessage")
}
// 按钮:更新消息内容
Button(onClick = { reportMessage = "用户修改后的新消息" } ) {
Text(reportMessage)
}
}
@Composable
fun MessageReporter(currentMessage: String) {
Log.d("Report","MessageReporter----start---")
// 错误做法:不使用rememberUpdatedState
LaunchedEffect(Unit) {
delay(5000) // 延迟3秒上报
// 问题:即使currentMessage已更新,仍会上报初始值
Log.d("Report", "错误上报: $currentMessage")
}
// 正确做法:必须使用rememberUpdatedState
val latestMessage by rememberUpdatedState(currentMessage)
LaunchedEffect(Unit) {
delay(10000) // 延迟3秒上报
// 确保上报的是最新值
Log.d("Report", "正确上报: $latestMessage")
}
Log.d("Report","MessageReporter----end---")
}
//日志打印
//初始化
2025-09-11 20:27:26.742 6847-6847 D MessageReporter----start---
2025-09-11 20:27:26.749 6847-6847 D MessageReporter----end---
//点击后
2025-09-11 20:27:29.077 6847-6847 D MessageReporter----start---
2025-09-11 20:27:29.077 6847-6847 D MessageReporter----end---
//延迟消息
2025-09-11 20:27:32.096 6847-6847 D 错误上报: 初始消息
2025-09-11 20:27:37.098 6847-6847 D 正确上报: 用户修改后的新消息
2.2.4 derivedStateOf
通过派生状态的结果去重,避免因 "依赖状态频繁变化但结果不变" 导致的重组。
示例:只有当userName和password都不为空时才需要重组按钮
kotlin
@Composable
fun LoginScreen() {
// 状态源1:用户名输入
var username by remember { mutableStateOf("") }
// 状态源2:密码输入
var password by remember { mutableStateOf("") }
//错误写法,每次输入username或者password时,isLoginEnabled都会导致按钮重组
//val isLoginEnabled = username.isNotEmpty() && password.isNotEmpty()
//正确写法 用derivedStateOf组合两个状态,判断按钮是否可点击
val isLoginEnabled by remember {
derivedStateOf {
// 同时依赖username和password两个状态
username.isNotEmpty() && password.isNotEmpty()
}
}
// 依赖isLoginEnabled的按钮
Button(
onClick = { /* 登录逻辑 */ },
enabled = isLoginEnabled
) {Text("登录")}
}
2.2.5 标记稳定类型 :@Stable/@Immutable
自定义数据类未标记稳定类型,Compose 无法判断其是否变化,可能会 "过度谨慎" 地触发重组。 原因:Compose 默认认为 "未标记的自定义类是不稳定的",即使所有属性都是val。
2.2.5.1 @Stable/@Immutable 使用
所以如下未标记时,在Test()重组时,即使person对象本身和name、age没有发生变化,都可能导致name Text()或者age Text()发生重组(过度谨慎重组)
优化方法:添加@Stable/@Immutable标记,防止Compose因过度谨慎带来的不必要的重组。
kotlin
// 未标记稳定类型的自定义数据类
data class Person(val name: String, val age: Int)
//@Immutable(完全不可变,name和age都是val不可变类型)
//data class Person(val name: String, val age: Int)
//@Stable(稳定类型(不一定完全不可变),name是var可变,age是不可变类型)
//data class Person(var name: String, val age: Int)
@Composable
fun Test(person: Person){
Column {
//名称
Text(text = person.name)
//年龄
Text(text = "${person.age}",)
}
}
2.2.5.2 @Stable/@Immutable 区别
-
@Immutable 标记的完全不可变的类,只要引用没变Compose就认为内部数据一定没变,不需要重组。特性:
类的所有属性都是val(不可变)
less
//正确
@Immutable
data class Book(
val id: Int, // val 不可变
val title: String // val 不可变
)
// 错误:包含 var 属性
@Immutable
data class Book(
val id: Int,
var title: String // var 可变,违反条件
)
// Book 对象做为Composable入参
@Composable
fun Test(book: Book){
Column {
//名称
Text(text = person.title)
}
}
所有属性类型本身也是不可变的(或被@Immutable标记)
less
// 自定义不可变类(满足 @Immutable 条件)
@Immutable
data class Author(
val name: String, // String 是不可变类型
val age: Int // Int 是不可变类型
)
// 引用 @Immutable 类型的属性
@Immutable
data class Book(
val id: Int,
val title: String,
// Author 被 @Immutable 标记,满足条件
// 如果Author 没有被 @Immutable 标记,则不满足条件
val author: Author
)
// 错误:属性类型是可变的 MutableList
@Immutable
data class Book(
val id: Int,
val tags: MutableList<String> // MutableList 是可变类型,违反条件
)
类本身没有任何可修改状态(包括间接引用对象)
less
// 最底层:不可变类型
@Immutable
data class Address(
val city: String,
val street: String
)
// 中间层:引用不可变类型
@Immutable
data class User(
val name: String,
val address: Address // Address 是 @Immutable 类型
)
// 顶层:引用不可变类型
@Immutable
data class Order(
val id: Int,
val user: User // User 是 @Immutable 类型
)
-
@Stable 标记稳定的类(可存在可变属性),引用没变且内部状态能被追踪,Compose就能精准判断只有内部状态发生变化时才触发重组,避免无效重组。特性:
- 类中存在可变属性var
- 可变属性必须是可被追踪的
kotlin
@Stable
class User {
// var age = 18 普通变量,不可追踪、被观察,变化后不会触发重组
var age by mutableStateOf(18) // 变化可追踪
}
// 用 User 作为入参的 Composable
@Composable
fun UserInfo(user: User) {
Text("年龄:${user.age}") // 依赖 user.age
}
也就是说要么引用变了(肯定要检查并重组),要么内部状态变了(Compose 能感知到),不会出现引用和内部状态都不变的情况下重组了,也不会出现 "状态变了但 Compose 不知道" 的情况。因此 Compose 可以放心地优化重组逻辑,既不会漏更 UI,也不会做无用功。
2.2.5.3 总结
使用@Stable/@Immutable标记自定义类目的:因Compose 默认认为 "未标记的自定义类是不稳定的",可能会发生"过度谨慎"重组。- 添加@Immutable注解完全不可变的类,Compose只有引用对象发生变化时需要重组,自定义类中不可存在可变属性。
- 添加@Stable注解稳定的类(可存在可变属性),Compose只有引用对象发生变化或内部状态发生变化时需要重组,自定义类中允许存在可变属性。
2.2.6 snapshotFlow 高频防抖
kotlin
@Composable
fun SearchInput() {
var searchQuery by remember { mutableStateOf("") }
// 错误:每次输入字符都会触发重组,直接执行搜索,高频调用
//LaunchedEffect(searchQuery) {
// // 模拟搜索网络请求
// Log.d("Search", "搜索:$searchQuery")
// }
// 正确:将状态转为Flow,添加300ms防抖,仅停止输入后执行
LaunchedEffect(Unit) {
snapshotFlow { searchQuery } // 转换Compose状态为Flow
.debounce(300) // 防抖:300ms内无变化才继续
.collect { query ->
if (query.isNotEmpty()) {
Log.d("Search", "搜索:$query") // 仅停止输入后执行
}
}
}
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
label = { Text("输入搜索内容") }
)
}
2.3 优化重组效率:
让必须重组的过程 "更快"
2.3.1 减少可组合函数内的耗时操作
可组合函数应只做 "描述 UI" 的轻量操作,禁止在其中直接执行 IO、网络请求、复杂计算。
kotlin
@Composable
fun UserProfile(userId: String) {
//错误示例,在组合函数中直接请求网络,每次重组时都会发起网络请求
//fetchDataFromNetwork() // 网络请求(副作用)
var user by remember { mutableStateOf<User?>(null) }
// 副作用:网络请求,依赖 userId(userId 变化时会重新执行)
LaunchedEffect(userId) {
// 耗时操作放在协程中,不阻塞主线程
user = api.fetchUser(userId) // 网络请求(副作用)
}
if (user != null) {
Text("Name: ${user?.name}")
} else {
CircularProgressIndicator()
}
}
2.3.2 避免在重组中创建新对象
每次重组时创建新对象(如Lambda)会被 Compose 视为 "参数变化",触发子组件重组。 温故而知新,之前实际开发中也都没注意到这些。- Lambda
kotlin
//错误示例
@Composable
fun UserProfile(user: User) {
// 每次重组都会创建新的 Lambda 实例
Button(onClick = {
// 处理点击事件
navigateToUserDetail(user.id)
}) {
Text("查看详情")
}
}
//正确示例
@Composable
fun UserProfile(user: User) {
// 无依赖的 remember,仅在首次组合时创建一次 Lambda
val onClick = remember {
{ navigateToUserDetail(user.id) }
}
Button(onClick = onClick) {
Text("查看详情")
}
}
```
```