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("查看详情")
    }
}
```
```
相关推荐
alexhilton6 小时前
学会在Jetpack Compose中加载Lottie动画资源
android·kotlin·android jetpack
ljt27249606614 天前
Compose笔记(六十一)--SelectionContainer
android·笔记·android jetpack
QING6184 天前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
惟恋惜4 天前
Jetpack Compose 的状态使用之“界面状态”
android·android jetpack
喜熊的Btm4 天前
探索 Kotlin 的不可变集合库
kotlin·android jetpack
惟恋惜5 天前
Jetpack Compose 界面元素状态(UI Element State)详解
android·ui·android jetpack
惟恋惜5 天前
Jetpack Compose 多页面架构实战:从 Splash 到底部导航,每个 Tab 拥有独立 ViewModel
android·ui·架构·android jetpack
alexhilton6 天前
Jetpack Compose 2025年12月版本新增功能
android·kotlin·android jetpack
モンキー・D・小菜鸡儿7 天前
Android Jetpack Compose 基础控件介绍
android·kotlin·android jetpack·compose
darryrzhong9 天前
FluxImageLoader : 基于Coil3封装的 Android 图片加载库,旨在提供简单、高效且功能丰富的图片加载解决方案
android·github·android jetpack