Compose 重组优化

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("查看详情")
    }
}
```
```
相关推荐
行墨5 小时前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack
alexhilton2 天前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Pika3 天前
深入浅出 Compose 测量机制
android·android jetpack·composer
fundroid4 天前
掌握 Compose 性能优化三步法
android·android jetpack
ljt27249606618 天前
Compose笔记(五十一)--rememberTextMeasurer
android·笔记·android jetpack
wxson728213 天前
【用androidx.camera拍摄景深合成照片】
kotlin·android jetpack·androidx
天花板之恋13 天前
Compose Navigation总结
android jetpack
alexhilton14 天前
灵活、现代的Android应用架构:完整分步指南
android·kotlin·android jetpack
4z3315 天前
Jetpack Compose重组原理(一):快照系统如何精准追踪状态变化
前端·android jetpack