一、Compose的编译时魔法
Jetpack Compose的核心创新在于编译时代码生成。传统View系统在运行时通过反射和XML解析构建UI,而Compose在编译时将@Composable函数直接转化为UI树结构的描述代码,消除了反射的开销。
1.1 @Composable函数的本质
// Compose编译器插件(composecompiler)处理的转换:
//
// 原始代码:
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
//
// 编译器生成的等效代码(伪代码):
fun Greeting(name: String, composer: Composer) {
composer.startReplaceableGroup(42) // 组标记
if (composer.isTraceEventEnabled) {
composer.traceEventStart(42, "Greeting", -1, -1)
}
val $composer = composer.composeableLambda(42) { c ->
Text("Hello $name")
}
$composer()
if (composer.isTraceEventEnabled) {
composer.traceEventEnd()
}
composer.endReplaceableGroup()
}
// 关键点:
// - 每个@Composable函数都接收隐式的Composer参数
// - 编译器自动注入composer.begin/composer.end调用
// - composeableLambda创建可组合lambda供重组使用
1.2 语义树(Semantic Tree)的构建
// Compose UI的层级结构(不同于View的ViewGroup树)
//
// Composables函数签名决定了它们如何被加入重组范围:
//
// Skippable函数(可跳过的重组):
@Composable fun SkippableContent(x: Int) {
// 当x未变化时,Compose跳过此函数的执行
Text("Value: $x")
}
//
// restartable函数(可重启的重组):
@Composable fun RestartableContent() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
// count变化时,从这里开始重启(而非父级)
}
// 组的概念:
// - replaceableGroup:值变化时整体替换
// - restartableGroup:值变化时可从当前位置重启
// - movableContentGroup:支持在组间移动而不重建
// 编译报告(打开composeCompilerReports):
// composeMetrics.gradle
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>.configureEach {
kotlinOptions {
freeCompilerArgs += listOf(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsOutput=..."
)
}
}
二、重组(Recomposition)的最小化策略
2.1 稳定的类型与stable冠名
// Compose如何判断"需要重组":
// 1. 基础类型(Int/String/Boolean等)→ stable,自动比较
// 2. 已知stable的类 → 用equals比较
// 3. 未知类型 → 假设unstable,每次都重组
// 手动标记stable(用于自定义类型):
@Stable
class UiState(
val userName: String,
val isLoading: Boolean
)
// @Stable的契约:
// - equals()必须基于值而非身份
// - 所有字段也必须是stable的
// - 相当于告诉编译器"这个类可以安全比较"
import androidx.compose.runtime.Stable
// 常用stable注解:
@Stable // 单个类
@Immutable // 不可变类(比@Stable更强的保证)
// 常见unstable陷阱:
data class Result<T>(val data: T?, val error: Throwable?)
// ⚠️ T是泛型,Compose无法判断其稳定性
// 解决:@Stable class Result<T : Any>(...)
// Lambda的稳定性:
val onClick: () -> Unit // 稳定(函数引用是stable的)
val onClick: (Int) -> Unit // 稳定
// remember { { value -> doSomething(value) } }
// ↑ remember包裹使lambda稳定
2.2 DerivedStateOf与Snapshot机制
// DerivedStateOf:防止派生状态过度更新
//
// 问题:多个状态同时变化时,派生计算可能被执行多次
var firstName by mutableStateOf("")
var lastName by mutableStateOf("")
val fullName = firstName + " " + lastName // 每次输入都重算
// 解决:derivedStateOf只在相关状态变化时重算
val fullName by derivedStateOf {
firstName + " " + lastName // 只有firstName/lastName变化才重算
}
// Snapshot系统(Compose的状态管理核心):
//
// Snapshot是一种软件事务内存(STM)的轻量实现
// 多个状态写入被合并为一次UI更新
//
// apply {
snapshot.enter {
a.value = 1
b.value = 2 // 两个写入在同一次快照中
}
}
// → UI只更新一次(而非两次)
// Side Effect的正确管理方式:
@Composable
fun AsyncContent() {
val scope = rememberCoroutineScope()
// LaunchedEffect:在Composition中启动协程
LaunchedEffect(key1 = userId) {
// 副作用:网络请求
val data = fetchUser(userId)
viewModel.updateState(data)
}
// rememberCoroutineScope:在Composable外部启动协程
Button(onClick = {
scope.launch {
doBackgroundWork()
}
}) { Text("执行") }
}
三、性能优化的核心策略
3.1 避免不必要的重组范围
// 问题代码:父级状态变化导致整个UI重建
@Composable
fun BadParent() {
var showDialog by remember { mutableStateOf(false) }
HeavyList() // ⚠️ showDialog变化时,HeavyList也会重组
Button(onClick = { showDialog = true }) { Text("打开") }
}
// 优化:用key或独立函数隔离重组范围
@Composable
fun GoodParent() {
var showDialog by remember { mutableStateOf(false) }
HeavyList() // ✅ 独立函数,重组范围隔离
DialogButton(showDialog) { showDialog = true }
}
@Composable
private fun DialogButton(
show: Boolean,
onShow: () -> Unit
) {
// 只有show变化时才重组
Button(onClick = onShow) { Text("打开") }
}
// 使用key()强制重组隔离:
@Composable
fun ListWithKeys(items: List<Item>) {
LazyColumn {
items(
items = items,
key = { item -> item.id } // ✅ key使得只有变化的项目重建
) { item ->
ListItem(item)
}
}
}
// 最佳实践:越靠近状态消费者的代码粒度越细
// 重组的最小单位是"可组合函数的调用点"
3.2 LazyColumn大数据量优化
// LazyColumn 99%场景的性能问题来源:
// 1. Item不稳定导致重建
// 2. 每次重组重新Layout
// 3. 绘制内容过大
// 优化①:固定Item高度(避免测量抖动)
LazyColumn {
items(
list,
key = { it.id }
) { item ->
// 固定高度估算
LazyItemScope
}
}
// 估算高度缓存(减少测量):
@Composable
fun FixedHeightItem(item: Item) {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 72.dp) // 最小高度
) { /* content */ }
}
// 优化②:内容类型(ContentType)减少重组
LazyColumn {
items(
items = mixedList,
contentType = { item ->
when (item) { is Header -> "header"; is Row -> "row" }
}
) { item -> /* ... */ }
}
// 优化③:异步图片加载(Coil)
LazyColumn {
items(items) { item ->
AsyncImage(
model = item.imageUrl,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f),
contentScale = ContentScale.Crop
)
}
}
四、测量-布局-绘制的性能陷阱
// Compose的绘制管线:
// Composition → Layout → Draw
//
// 每个阶段的成本比例(近似):
// Composition: ~30%(创建UI节点)
// Layout: ~50%(测量+定位,最耗时)
// Draw: ~20%(光栅化+上传GPU)
// 陷阱①:在Layout阶段执行重计算
@Composable
fun BadLayout() {
val layoutInfo = remember { computeExpensiveLayout() } // ⚠️ 每次重组重算
}
// 正确:
@Composable
fun GoodLayout() {
val layoutInfo = remember { computeExpensiveLayout() } // ✅ remember缓存
}
// 陷阱②:Modifier链过长
Modifier
.fillMaxWidth()
.padding(16.dp)
.background(Color.Gray)
.clickable { } // ⚠️ 过多的修饰符
.pointerInput(Unit) { } // ⚠️ 每个pointerInput都可能启动新的协程
// 合并相近修饰符:
Modifier
.fillMaxWidth()
.background(Color.Gray)
.combinedClickable( // ✅ combinedClickable = clickable + longClickable
onClick = {},
onLongClick = {}
)
// 陷阱③:绘制阶段的无用重绘
@Composable
fun ExpensiveCanvas() {
Canvas(modifier = Modifier.drawBehind {
// ⚠️ 每次重组都执行全部绘制逻辑
for (i in 0..1000) {
drawCircle(...) // 1000个圆
}
}) { }
}
// 优化:用remember缓存绘制结果
@Composable
fun OptimizedCanvas() {
val cachedDraw = remember {
drawDrawComponent { canvas ->
for (i in 0..1000) {
canvas.drawCircle(...)
}
}
}
Canvas(modifier = Modifier.graphicsLayer { cachedDraw }) {}
}