/ Android  Jetpack Compose  性能优化  重组  LazyColumn  Kotlin  稳定性注解  UI性能 

Jetpack Compose 性能优化实战:告别重组风暴与 LazyColumn 卡顿


封面

一、Compose 重组机制:为什么会卡?

Jetpack Compose 的核心思想是"声明式 UI"——状态改变时,框架自动触发受影响的 Composable 函数重新执行(即重组 Recomposition)。理想情况下,只有真正依赖某个 State 的组件才会重组,但实际开发中,不合理的代码结构会导致大量不必要的重组,从而引发 UI 卡顿。

常见的重组过多原因包括:

  • 将整个 ViewModel 状态作为参数传递,而不是只传所需字段

  • 在 Composable 内部直接读取 StateFlow,导致整个函数随任意状态变化而重组

  • 使用 Lambda 捕获了外部可变变量,Compose 无法判断其稳定性

  • 集合类型(List/Map)未使用 @Stable 或 ImmutableList

二、稳定性注解:告诉编译器「我是稳定的」

Compose 编译器在编译期会对每个参数进行稳定性分析。如果某个参数被认为是"不稳定的",Compose 就会在每次父组件重组时无条件地重组该子组件,即使参数值没有变化。

通过 @Stable 和 @Immutable 注解,可以手动告知编译器类型是稳定的:

// 告知 Compose 这个数据类是不可变的
@Immutable
data class UserProfile(
    val id: Long,
    val name: String,
    val avatarUrl: String
)

// 告知 Compose 这个类的字段变化会正确通知观察者
@Stable
class CartState {
    var itemCount by mutableStateOf(0)
    var totalPrice by mutableStateOf(0.0)
}

对于 List 类型,推荐使用 kotlinx.collections.immutable 提供的 ImmutableList:

// build.gradle.kts
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")

// 在 ViewModel 中
val userList: StateFlow<ImmutableList<User>> = _userList
    .map { it.toImmutableList() }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), persistentListOf())

三、LazyColumn 优化:大列表不再掉帧

LazyColumn 是 Compose 中最常用也最容易踩坑的组件。以下几点对于性能至关重要:

3.1 必须提供稳定的 key

LazyColumn {
    items(
        items = userList,
        key = { user -> user.id }  // ✅ 稳定的业务 ID
    ) { user ->
        UserCard(user = user)
    }
}

没有 key 时,列表滚动或数据更新会导致所有可见 Item 全部重组。提供稳定 key 后,Compose 可以精确识别哪些 Item 发生了变化。

3.2 使用 contentType 提升 RecyclerView 式复用

LazyColumn {
    items(
        items = feedList,
        key = { it.id },
        contentType = { item -> item.type }  // 让 Compose 复用同类型 Item
    ) { item ->
        when (item.type) {
            FeedType.TEXT -> TextFeedCard(item)
            FeedType.IMAGE -> ImageFeedCard(item)
        }
    }
}

四、derivedStateOf 与 remember:避免无效计算

derivedStateOf 用于从一个或多个 State 派生出新 State,只有当计算结果真正改变时,才会触发依赖它的 Composable 重组:

@Composable
fun CartSummary(cartItems: List<CartItem>) {
    // ❌ 错误:每次重组都重新计算
    val totalPrice = cartItems.sumOf { it.price * it.quantity }

    // ✅ 正确:只有 totalPrice 计算结果变化时才触发重组
    val totalPrice by remember(cartItems) {
        derivedStateOf { cartItems.sumOf { it.price * it.quantity } }
    }

    Text("总价:¥${"%.2f".format(totalPrice)}")
}

类似地,remember { } 可缓存开销较大的对象,避免每次重组都重新创建:

val dateFormatter = remember { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) }

五、借助 Compose Compiler Report 精准定位问题

Compose 编译器可以输出详细的稳定性报告,帮助你找出哪些类/参数被标记为不稳定:

// build.gradle.kts
android {
    kotlinOptions {
        freeCompilerArgs += listOf(
            "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
                    project.buildDir.absolutePath + "/compose_reports"
        )
    }
}

执行 ./gradlew assembleDebug 后,在 build/compose_reports/ 目录下会生成 *-composables.txt 和 *-classes.txt 两份报告。前者列出每个 Composable 函数的重组状态(restartable/skippable),后者列出每个类的稳定性评估。

  • skippable:所有参数稳定,父组件重组时可以跳过 ✅

  • restartable but not skippable:有不稳定参数,需要优化 ⚠️

结合 Android Studio 的 Layout Inspector > Recomposition Counts 功能,可以直观看到每个组件的重组次数,快速定位热点组件。

六、实战建议与总结

以下是一份可以立即落地的 Compose 性能优化 Checklist:

  • ✅ 数据类加 @Immutable,避免不必要的重组

  • ✅ List 参数改用 ImmutableList

  • ✅ LazyColumn 始终提供 key 和 contentType

  • ✅ 派生状态使用 derivedStateOf,昂贵对象用 remember 缓存

  • ✅ 开启 Compose Compiler Report,定期检查 skippable 比率

  • ✅ 使用 Layout Inspector 的重组计数功能定位热点

  • ✅ ViewModel 中只暴露 UI 真正需要的最小状态切片

Compose 的性能优化本质上是"减少不必要的工作"——让框架知道哪些状态是稳定的,哪些计算结果可以被复用。掌握这套方法论后,你会发现大多数卡顿问题都可以在不改变业务逻辑的前提下得到解决。

发布评论

热门评论区: