Administrator
发布于 2026-08-25 / 24 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 6 篇:Compose 性能——从 60FPS 到 120FPS 的真实路径

上一篇我们聊了启动、内存、卡顿的整体方法论。从这一篇开始,进入高级篇,我们选择一个当下最热门也最容易被误用的方向深入:Jetpack Compose 性能。

Compose 的声明式 UI 极大地提升了开发效率,但“写起来爽”和“跑起来顺”是两件事。我见过太多团队:

  • 把 XML 直接翻译成 Compose,结果列表滚动掉帧。

  • 滥用 remember 和 mutableStateOf,状态更新反而更不可预测。

  • 把重组(recomposition)当成“黑盒”,出了问题只会加 LaunchedEffect 碰运气。

这篇文章我会把 Compose 性能优化拆成可执行的 checklist:从理解重组,到 Lazy 列表优化,再到工具定位,目标是让你的 Compose 界面稳在 60FPS,高端机冲刺 120FPS。


一、重组不是敌人,错误的重组才是

Compose 的核心优势是智能重组:当状态变化时,只重新执行依赖该状态的 Composable。

但“智能”有前提:你的状态必须分得够细、够稳定。

1.1 重组什么时候发生?

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Column {
        Text("Count: $count")          // 每次 count 变都会重组
        Button(onClick = { count++ }) {
            Text("+1")                 // 不会重组,因为不依赖 count
        }
    }
}

Button 里的 Text 不会重组,因为它不读取 count。这是 Compose 编译器自动做的细粒度追踪。

1.2 常见错误:把无关状态往上提

@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()  // 整个 UIState
    Column {
        Header(uiState.user)
        Body(uiState.posts)
        Footer(uiState.settings)
    }
}

只要 uiState 里任意字段变化,整个 ProfileScreen 都会重组。

优化:把状态拆成更小的粒度。

@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val user by viewModel.user.collectAsState()
    val posts by viewModel.posts.collectAsState()
    val settings by viewModel.settings.collectAsState()
    Column {
        Header(user)
        Body(posts)
        Footer(settings)
    }
}

现在 posts 更新时,Header 和 Footer 不会重组。


二、稳定性:Compose 跳过重組的关键

Compose 会跳过重组的一个前提是:传入的参数被认为是稳定的(Stable)。

2.1 什么算 Stable?

  • 原始类型:Int、String、Boolean

  • 不可变集合:List、Map 的实现如果是不可变的

  • @Stable 或 @Immutable 标注的类

@Immutable
data class User(
    val id: String,
    val name: String,
    val avatar: String
)

2.2 不稳定类引发重组

data class User(var name: String)  // var → 不稳定

@Composable
fun Greeting(user: User) {
    Text("Hello, ${user.name}")
}

只要 User 实例被重新创建,哪怕 name 没变,Compose 也会认为它不稳定,触发重组。

修复:用 val、加 @Immutable、或者用 remember 缓存实例。

2.3 用 Layout Inspector 看稳定性

Android Studio 的 Layout Inspector → Compose 面板会标出每个 Composable 的参数是否稳定:

  • 绿色 = Stable

  • 黄色 = Unstable

  • 红色 = 每次都变

定位黄色节点,就是性能优化的切入点。


三、LazyColumn / LazyRow:列表优化主战场

列表是 Compose 里最容易掉帧的地方。核心原则只有两条:

  1. item 必须轻

  2. key 必须对

3.1 用 key 保证复用

LazyColumn {
    items(posts) { post ->           // 没有 key,滑动时复用效率低
        PostItem(post)
    }
}

// 正确
LazyColumn {
    items(
        items = posts,
        key = { post -> post.id }    // 给每个 item 唯一 key
    ) { post ->
        PostItem(post)
    }
}

没有 key 时,LazyColumn 只能按位置复用;有 key 后,可以按内容复用,动画也更自然。

3.2 item 内部不要放大图

一个常见错误:在 LazyColumn 的 item 里直接加载网络大图。

@Composable
fun PostItem(post: Post) {
    Column {
        AsyncImage(
            model = post.cover,       // 大图原图
            contentDescription = null,
            modifier = Modifier.fillMaxWidth()
        )
        Text(post.title)
    }
}

如果封面图是 2K 分辨率,GPU 解码和上传都会吃资源。

修复:

  • 服务端裁剪到合适尺寸。

  • 用 Glide / Coil 的 size() 或 scale 参数限制。

  • 列表项里避免使用 Modifier.blur()、shadow() 等昂贵效果。

3.3 derivedStateOf:减少无效重组

列表滚动时,经常需要根据 LazyListState.firstVisibleItemScrollOffset 做顶部导航栏的渐变。

val lazyListState = rememberLazyListState()
val showTitle by remember {
    derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}

derivedStateOf 会在依赖的多个状态变化时,只在输出结果变化时才触发重组。比直接读取 offset 高效得多。


四、副作用:LaunchedEffect、DisposableEffect、SideEffect 怎么选?

Compose 里处理副作用有三个主要 API,很多人用混了。

API

使用场景

LaunchedEffect

启动协程,处理一次性异步任务

DisposableEffect

需要清理资源,如注册/反注册监听器

SideEffect

每次重组成功后将状态同步到非 Compose 世界

4.1 LaunchedEffect 的 key 用错会死循环

// 错误:count 变化 -> 重启 effect -> 又改 count -> 死循环
var count by remember { mutableStateOf(0) }
LaunchedEffect(count) {
    count++
}

正确做法:LaunchedEffect 的 key 应该稳定,内部只触发与 key 相关的副作用。


五、从 60FPS 到 120FPS:高端机专项优化

120Hz 屏幕意味着每帧只有 8.3ms,比 60Hz 的 16.6ms 严苛一倍。

5.1 减少 Measure / Layout 层数

Compose 的布局系统比 XML 更灵活,但也更容易嵌套过深。

// 不推荐
Column {
    Box {
        Column {
            Row { ... }
        }
    }
}

能用 ConstraintLayout 拉平的,就别多层嵌套。

5.2 异步加载 Heavy Content

复杂页面不要一次性全画出来:

  • 用 SubcomposeLayout 做按需组合。

  • 大图、富文本走异步线程预计算。

  • 非首屏内容用 LazyColumn 延迟加载。

5.3 开启 GPU Profiling

Android Studio 的 GPU Rendering Profile 能直观显示每帧耗时:

  • 绿线以下 = 达标

  • 黄线 = 接近掉帧

  • 红线 = 掉帧

结合 Macrobenchmark 做自动化帧率测试:

benchmarkRule.measureRepeated(
    packageName = "com.example.app",
    metrics = listOf(FrameTimingMetric()),
    iterations = 5,
    setupBlock = { pressHome(); startActivityAndWait() }
) {
    scrollRecycler()
}

六、Compose 性能 Checklist

层级

检查项

架构

状态是否下沉到最小作用域?

稳定

数据类是否用 val / @Immutable?

列表

LazyColumn 是否加了 key?

图片

是否限制了加载尺寸?

副作用

LaunchedEffect 是否会触发循环更新?

工具

是否用 Layout Inspector / Macrobenchmark 定位过瓶颈?


小结

Compose 性能优化的核心不是“少写重组”,而是:

  1. 让状态尽可能稳定且分散。

  2. 列表加 key、图片控尺寸、布局少嵌套。

  3. 副作用 API 选对了,避免隐性循环更新。

  4. 用 Layout Inspector 和 Macrobenchmark 量化结果。


下篇预告

下一篇《第 7 篇:Android 跨进程通信:Binder、AIDL、Messenger 与 MMKV 选型》会进入 Android 系统的核心通信机制。我们会讲:

  1. Binder 为什么比 Socket / Pipe 快一个数量级。

  2. AIDL 和 Messenger 的选型边界。

  3. 多进程架构下 MMKV / ContentProvider 怎么选。


参考阅读

  • Jetpack Compose 官方性能指南

  • 《Compose 进阶实战》第 4 章

  • Android 开发者文档:Inspect Compose Layout

  • 上一篇文章:第 5 篇《性能优化:启动、内存、卡顿的一站式排查手册》


评论