/ Android  Compose  Kotlin  性能优化  LazyList  Recomposition  Jetpack  UI开发 

Jetpack Compose 性能优化实战:从重组原理到 60fps 实践指南


封面

Jetpack Compose 自正式发布以来,已成为 Android 声明式 UI 开发的标准选择。然而随着项目规模增大,许多开发者开始遭遇 Compose 的性能问题:卡顿、掉帧、不必要的重组(Recomposition)。本文从底层原理出发,系统讲解如何排查和优化 Compose 的渲染性能,帮助你将帧率稳定在 60fps 甚至 120fps。

一、理解 Compose 的渲染三阶段

要优化性能,首先要理解 Compose 的渲染流程。Compose 将 UI 渲染分为三个阶段:

  • Composition(组合):执行 Composable 函数,构建 UI 树(Slot Table)

  • Layout(布局):测量每个节点的尺寸与位置,单次遍历即可完成(相比 View 避免多次测量)

  • Drawing(绘制):将布局结果绘制到画布(Canvas)上

理解这三个阶段的关键点:不是每次 State 变化都会触发完整的三阶段。Compose 有一套智能跳过机制——如果某个 Composable 的输入没有变化,它可以被跳过(skipped)。这个跳过机制是性能优化的核心。

// Compose 内部的跳过判断逻辑示意
// 如果所有参数都 "stable" 且值没变化,则跳过重组
@Composable
fun UserCard(user: User) { // User 如果是 data class,默认 stable
    Text(text = user.name)
    Text(text = user.email)
}

二、重组(Recomposition)的陷阱与优化策略

不必要的重组是 Compose 性能问题的头号原因。以下是常见的重组陷阱:

陷阱1:Lambda 捕获导致重组扩散

// ❌ 错误写法:每次父组件重组,onClick 都是新的 Lambda 实例
@Composable
fun ParentComposable(items: List<Item>) {
    items.forEach { item ->
        ItemRow(item = item, onClick = { doSomething(item) })
    }
}

// ✅ 正确写法:使用 remember 缓存 Lambda
@Composable
fun ParentComposable(items: List<Item>) {
    items.forEach { item ->
        val onClick = remember(item.id) { { doSomething(item) } }
        ItemRow(item = item, onClick = onClick)
    }
}

陷阱2:unstable 类导致无法跳过

// ❌ List<T> 被 Compose 认为是 unstable(因为它是 MutableList 的接口)
@Composable
fun ItemList(items: List<String>) { ... }

// ✅ 方案1:使用 @Stable 或 @Immutable 注解包装
@Immutable
data class ImmutableList<T>(val items: List<T>)

// ✅ 方案2:使用 kotlinx.collections.immutable
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")

@Composable
fun ItemList(items: ImmutableList<String>) { ... }

陷阱3:读取 State 的位置影响重组范围

将 State 的读取尽可能下移到最小的 Composable 范围内,是减少重组范围的关键技巧。

// ❌ 在父级读取 State,导致整个父组件重组
@Composable
fun Screen(viewModel: MyViewModel) {
    val uiState by viewModel.uiState.collectAsState()
    Column {
        Header(title = uiState.title)    // 只有这里需要 title
        Body(content = uiState.content)  // 只有这里需要 content
    }
}

// ✅ 将 State 读取下移,各子组件独立重组
@Composable
fun Screen(viewModel: MyViewModel) {
    Column {
        Header(titleFlow = viewModel.titleFlow)
        Body(contentFlow = viewModel.contentFlow)
    }
}

三、使用 Compose 编译器报告分析重组

光靠肉眼审查代码很低效,Compose 编译器提供了强大的报告工具,可以直接告诉你哪些 Composable 是 restartable、skippable 或 unstable 的。

开启编译器报告

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

编译后在 build/compose_reports/ 目录下会生成报告文件,其中 *-composables.txt 列出每个 Composable 的稳定性分析:

restartable skippable scheme("[0, 65536]") fun UserCard(
  stable user: User
)

restartable scheme("[0, 65536]") fun OrderList(
  unstable orders: List<Order>  // ⚠️ 这里!无法被跳过
)

使用 Layout Inspector 实时观测重组

Android Studio 的 Layout Inspector 支持 Compose,可以实时高亮哪些 Composable 正在重组,并显示重组次数。开启路径:View → Tool Windows → Layout Inspector,选中 App Process 后勾选"Show Recomposition Counts"。

四、LazyList 性能优化:避免常见的 100ms 卡顿

LazyColumn / LazyRow 是 Compose 中最常用的列表组件,也是最容易踩坑的地方。

1. key 参数:避免无效的 item 重组

// ❌ 没有 key,数据变化时所有 item 都可能重组
LazyColumn {
    items(items) { item ->
        ItemCard(item)
    }
}

// ✅ 指定稳定的 key,Compose 只重组真正变化的 item
LazyColumn {
    items(items, key = { it.id }) { item ->
        ItemCard(item)
    }
}

2. contentType:加速 item 复用

// 当列表有多种类型时,指定 contentType 可以提高复用效率
LazyColumn {
    items(
        items = feedItems,
        key = { it.id },
        contentType = { it.type } // "post", "ad", "story" 等
    ) { item ->
        when (item.type) {
            "post" -> PostCard(item)
            "ad" -> AdBanner(item)
            else -> StoryCard(item)
        }
    }
}

3. 预加载更多内容(prefetchDistance)

// 自定义 LazyListState 的预加载距离
val state = rememberLazyListState()
val prefetchState = rememberLazyListPrefetchState(
    prefetchScheduler = AsyncPrefetchScheduler()
)
LazyColumn(
    state = state,
    prefetchState = prefetchState
) { ... }

4. 避免在 LazyList 中嵌套滚动列表

在 LazyColumn 中嵌套 LazyRow 时,内层 LazyRow 的高度必须固定,否则会导致无限高度测量异常:

LazyColumn {
    item {
        LazyRow(
            modifier = Modifier.height(120.dp) // ✅ 必须固定高度
        ) {
            items(horizontalItems) { ItemChip(it) }
        }
    }
}

五、状态管理对性能的影响

不合理的状态管理会让本可避免的重组频繁发生。

优先选择细粒度 State 而非大对象

// ❌ 一个大的 data class State,任何字段变化都触发全量重组
data class ScreenState(
    val title: String,
    val isLoading: Boolean,
    val items: List<Item>,
    val errorMsg: String?
)
val state by viewModel.screenState.collectAsState()

// ✅ 拆分 State,按需收集
val title by viewModel.title.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val items by viewModel.items.collectAsState()

derivedStateOf:避免 State 衍生导致的过度重组

// 场景:根据列表滚动位置判断是否显示"回到顶部"按钮
val listState = rememberLazyListState()

// ❌ 每次滚动都会触发 showTopButton 的重组
val showTopButton = listState.firstVisibleItemIndex > 0

// ✅ 只有 Boolean 值真正变化时才触发重组
val showTopButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

六、图片加载与异步操作最佳实践

图片加载和网络请求是 Android 应用中最常见的异步操作,处理不当会严重影响 Compose 性能。

Coil 3 + AsyncImage 最佳实践

// build.gradle.kts
implementation("io.coil-kt.coil3:coil-compose:3.1.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0")

// 使用方式(自动处理内存缓存、磁盘缓存)
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(imageUrl)
        .crossfade(true)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .build(),
    contentDescription = "User avatar",
    contentScale = ContentScale.Crop,
    modifier = Modifier.size(48.dp).clip(CircleShape)
)

LaunchedEffect vs SideEffect vs DisposableEffect

// LaunchedEffect:用于一次性或 key 变化时触发的挂起协程
LaunchedEffect(userId) {
    viewModel.loadUserData(userId) // userId 变化时重新加载
}

// SideEffect:每次重组都执行(用于同步 Compose State 到非 Compose 系统)
SideEffect {
    analyticsTracker.setCurrentScreen(currentRoute)
}

// DisposableEffect:需要清理资源时使用
DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME) viewModel.refresh()
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}

七、实战 Checklist:Compose 性能优化清单

以下是一份可直接应用到项目的性能优化 Checklist:

  • 开启编译器报告,检查关键 Composable 是否为 skippable

  • 所有 data class 参数确保字段为 val(非 var),否则标注 @Stable/@Immutable

  • 集合类型(List/Map/Set)使用 kotlinx.collections.immutable 或自定义 @Immutable 包装

  • LazyList 的每个 items 都指定了稳定的 key 和 contentType

  • 滚动位置判断统一改用 derivedStateOf 包裹

  • Lambda 参数(onClick 等)在循环中用 remember(key) 缓存

  • State 读取尽可能下移到最小的 Composable 范围

  • 图片加载使用 Coil3/Glide Compose,启用内存+磁盘缓存

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

  • 使用 Macrobenchmark 对关键页面做帧率基准测试

Compose 性能优化是一个需要持续迭代的过程。建议在项目初期就养成良好习惯:每新增一个 Composable,就思考它的 stability 和 recomposition 范围。结合编译器报告和 Layout Inspector,大多数性能问题在开发阶段就能被发现和修复,而不是等到上线后才发现卡顿。

发布评论

热门评论区: