上一篇我们聊了启动、内存、卡顿的整体方法论。从这一篇开始,进入高级篇,我们选择一个当下最热门也最容易被误用的方向深入: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 里最容易掉帧的地方。核心原则只有两条:
item 必须轻
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,很多人用混了。
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
小结
Compose 性能优化的核心不是“少写重组”,而是:
让状态尽可能稳定且分散。
列表加 key、图片控尺寸、布局少嵌套。
副作用 API 选对了,避免隐性循环更新。
用 Layout Inspector 和 Macrobenchmark 量化结果。
下篇预告
下一篇《第 7 篇:Android 跨进程通信:Binder、AIDL、Messenger 与 MMKV 选型》会进入 Android 系统的核心通信机制。我们会讲:
Binder 为什么比 Socket / Pipe 快一个数量级。
AIDL 和 Messenger 的选型边界。
多进程架构下 MMKV / ContentProvider 怎么选。
参考阅读
Jetpack Compose 官方性能指南
《Compose 进阶实战》第 4 章
Android 开发者文档:Inspect Compose Layout
上一篇文章:第 5 篇《性能优化:启动、内存、卡顿的一站式排查手册》