第16课完成了架构设计的学习。本课深入CMP性能优化的核心实践:Compose三阶段渲染管线的优化切入点、重组优化策略(@Stable/@Immutable、key()、derivedStateOf、延迟读取)、Lazy布局的性能陷阱、iOS端Metal着色器编译停顿的预编译策略、纹理图集抖动修复、Kotlin/Native GC调优、基准测试工具链,以及find_degradation脚本与LazyGridImageView基准对比。本课内容基于CMP 1.12.1与Kotlin 2.3.20。 文末附有15道配套习题与本课知识点总结。
目录
- 一、性能优化的第一原则:先测量,再优化
- [1.1 绝不在Debug构建中做性能分析](#1.1 绝不在Debug构建中做性能分析)
- [1.2 性能优化的三个维度](#1.2 性能优化的三个维度)
- 二、三阶段渲染管线与优化切入点
- [2.1 渲染管线的三个阶段](#2.1 渲染管线的三个阶段)
- [2.2 延迟读取的实战](#2.2 延迟读取的实战)
- 三、重组优化策略
- [3.1 稳定类型:@Stable与@Immutable](#3.1 稳定类型:@Stable与@Immutable)
- [3.2 key()与Lazy列表](#3.2 key()与Lazy列表)
- [3.3 derivedStateOf:延迟读取与重组过滤](#3.3 derivedStateOf:延迟读取与重组过滤)
- [3.4 避免在重组期间分配对象](#3.4 避免在重组期间分配对象)
- 四、iOS端专项性能优化
- [4.1 Skiko渲染管线与Android的本质差异](#4.1 Skiko渲染管线与Android的本质差异)
- [4.2 Metal着色器编译停顿的诊断与修复](#4.2 Metal着色器编译停顿的诊断与修复)
- [4.3 纹理图集抖动的修复](#4.3 纹理图集抖动的修复)
- [4.4 Kotlin/Native GC调优](#4.4 Kotlin/Native GC调优)
- 五、Lazy布局的性能陷阱
- [5.1 避免为每个列表项创建LaunchedEffect](#5.1 避免为每个列表项创建LaunchedEffect)
- [5.2 避免在items()内部排序](#5.2 避免在items()内部排序)
- [5.3 AndroidView在LazyColumn中的性能问题](#5.3 AndroidView在LazyColumn中的性能问题)
- 六、基准测试工具链
- [6.1 Multiplatform Benchmark Framework的四种模式](#6.1 Multiplatform Benchmark Framework的四种模式)
- [6.2 find_degradation脚本:定位性能回退版本](#6.2 find_degradation脚本:定位性能回退版本)
- [6.3 LazyGridImageView:与原生实现的量化对比](#6.3 LazyGridImageView:与原生实现的量化对比)
- 七、性能分析工具速查
- 配套习题(15题)
- 本课知识点总结
一、性能优化的第一原则:先测量,再优化
1.1 绝不在Debug构建中做性能分析
这是性能优化中最容易被忽视的原则。Debug构建禁用了R8、内联和Strong Skipping,性能数据毫无意义。必须在Release构建(启用profileable)或benchmark构建类型上进行性能分析。
1.2 性能优化的三个维度
CMP性能优化分为三个维度:重组优化 (减少不必要的Composable重新执行)、渲染优化 (减少绘制阶段的工作量和GPU瓶颈)、内存优化(控制Kotlin/Native GC压力和纹理图集抖动)。三者相互关联------一次不必要的重组可能触发额外的布局和绘制,进而引发GPU层面的纹理重上传。
下面是CMP性能优化三个维度的关系图:
#mermaid-svg-qVdGuRJtTX61BUPN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qVdGuRJtTX61BUPN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qVdGuRJtTX61BUPN .error-icon{fill:#552222;}#mermaid-svg-qVdGuRJtTX61BUPN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qVdGuRJtTX61BUPN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qVdGuRJtTX61BUPN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qVdGuRJtTX61BUPN .marker.cross{stroke:#333333;}#mermaid-svg-qVdGuRJtTX61BUPN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qVdGuRJtTX61BUPN p{margin:0;}#mermaid-svg-qVdGuRJtTX61BUPN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qVdGuRJtTX61BUPN .cluster-label text{fill:#333;}#mermaid-svg-qVdGuRJtTX61BUPN .cluster-label span{color:#333;}#mermaid-svg-qVdGuRJtTX61BUPN .cluster-label span p{background-color:transparent;}#mermaid-svg-qVdGuRJtTX61BUPN .label text,#mermaid-svg-qVdGuRJtTX61BUPN span{fill:#333;color:#333;}#mermaid-svg-qVdGuRJtTX61BUPN .node rect,#mermaid-svg-qVdGuRJtTX61BUPN .node circle,#mermaid-svg-qVdGuRJtTX61BUPN .node ellipse,#mermaid-svg-qVdGuRJtTX61BUPN .node polygon,#mermaid-svg-qVdGuRJtTX61BUPN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qVdGuRJtTX61BUPN .rough-node .label text,#mermaid-svg-qVdGuRJtTX61BUPN .node .label text,#mermaid-svg-qVdGuRJtTX61BUPN .image-shape .label,#mermaid-svg-qVdGuRJtTX61BUPN .icon-shape .label{text-anchor:middle;}#mermaid-svg-qVdGuRJtTX61BUPN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qVdGuRJtTX61BUPN .rough-node .label,#mermaid-svg-qVdGuRJtTX61BUPN .node .label,#mermaid-svg-qVdGuRJtTX61BUPN .image-shape .label,#mermaid-svg-qVdGuRJtTX61BUPN .icon-shape .label{text-align:center;}#mermaid-svg-qVdGuRJtTX61BUPN .node.clickable{cursor:pointer;}#mermaid-svg-qVdGuRJtTX61BUPN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qVdGuRJtTX61BUPN .arrowheadPath{fill:#333333;}#mermaid-svg-qVdGuRJtTX61BUPN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qVdGuRJtTX61BUPN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qVdGuRJtTX61BUPN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qVdGuRJtTX61BUPN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qVdGuRJtTX61BUPN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qVdGuRJtTX61BUPN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qVdGuRJtTX61BUPN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qVdGuRJtTX61BUPN .cluster text{fill:#333;}#mermaid-svg-qVdGuRJtTX61BUPN .cluster span{color:#333;}#mermaid-svg-qVdGuRJtTX61BUPN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qVdGuRJtTX61BUPN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qVdGuRJtTX61BUPN rect.text{fill:none;stroke-width:0;}#mermaid-svg-qVdGuRJtTX61BUPN .icon-shape,#mermaid-svg-qVdGuRJtTX61BUPN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qVdGuRJtTX61BUPN .icon-shape p,#mermaid-svg-qVdGuRJtTX61BUPN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qVdGuRJtTX61BUPN .icon-shape .label rect,#mermaid-svg-qVdGuRJtTX61BUPN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qVdGuRJtTX61BUPN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qVdGuRJtTX61BUPN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qVdGuRJtTX61BUPN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CMP性能优化
重组优化
渲染优化
内存优化
减少不必要的Composable重新执行
减少绘制阶段工作量和GPU瓶颈
控制Kotlin/Native GC压力和纹理图集抖动
触发额外布局和绘制
GPU层面纹理重上传
二、三阶段渲染管线与优化切入点
2.1 渲染管线的三个阶段
Compose的每一帧都经过三个阶段,成本从左到右递增,重启范围从左到右递减:
| 阶段 | 执行内容 | 重启成本 | 触发条件 |
|---|---|---|---|
| 组合(Composition) | Composable函数、状态读取 | 高(整个作用域) | 在Composable函数体中读取状态 |
| 布局(Layout) | 测量+放置 | 中(子树) | 在measure lambda中读取状态 |
| 绘制(Drawing) | Canvas命令、graphicsLayer | 低(单个节点) | 在draw/graphicsLayer lambda中读取状态 |
核心优化策略:将状态读取尽可能推迟到后面的阶段。如果状态只在绘制阶段使用,就不要在组合阶段读取它------这样状态变化时只需重新执行绘制阶段,避免整个作用域的重组。
下面是三阶段渲染管线的流程图:
#mermaid-svg-t3qnO98NheqaE5xu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t3qnO98NheqaE5xu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t3qnO98NheqaE5xu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t3qnO98NheqaE5xu .error-icon{fill:#552222;}#mermaid-svg-t3qnO98NheqaE5xu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t3qnO98NheqaE5xu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t3qnO98NheqaE5xu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t3qnO98NheqaE5xu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t3qnO98NheqaE5xu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t3qnO98NheqaE5xu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t3qnO98NheqaE5xu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t3qnO98NheqaE5xu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t3qnO98NheqaE5xu .marker.cross{stroke:#333333;}#mermaid-svg-t3qnO98NheqaE5xu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t3qnO98NheqaE5xu p{margin:0;}#mermaid-svg-t3qnO98NheqaE5xu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-t3qnO98NheqaE5xu .cluster-label text{fill:#333;}#mermaid-svg-t3qnO98NheqaE5xu .cluster-label span{color:#333;}#mermaid-svg-t3qnO98NheqaE5xu .cluster-label span p{background-color:transparent;}#mermaid-svg-t3qnO98NheqaE5xu .label text,#mermaid-svg-t3qnO98NheqaE5xu span{fill:#333;color:#333;}#mermaid-svg-t3qnO98NheqaE5xu .node rect,#mermaid-svg-t3qnO98NheqaE5xu .node circle,#mermaid-svg-t3qnO98NheqaE5xu .node ellipse,#mermaid-svg-t3qnO98NheqaE5xu .node polygon,#mermaid-svg-t3qnO98NheqaE5xu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t3qnO98NheqaE5xu .rough-node .label text,#mermaid-svg-t3qnO98NheqaE5xu .node .label text,#mermaid-svg-t3qnO98NheqaE5xu .image-shape .label,#mermaid-svg-t3qnO98NheqaE5xu .icon-shape .label{text-anchor:middle;}#mermaid-svg-t3qnO98NheqaE5xu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-t3qnO98NheqaE5xu .rough-node .label,#mermaid-svg-t3qnO98NheqaE5xu .node .label,#mermaid-svg-t3qnO98NheqaE5xu .image-shape .label,#mermaid-svg-t3qnO98NheqaE5xu .icon-shape .label{text-align:center;}#mermaid-svg-t3qnO98NheqaE5xu .node.clickable{cursor:pointer;}#mermaid-svg-t3qnO98NheqaE5xu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-t3qnO98NheqaE5xu .arrowheadPath{fill:#333333;}#mermaid-svg-t3qnO98NheqaE5xu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-t3qnO98NheqaE5xu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-t3qnO98NheqaE5xu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t3qnO98NheqaE5xu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t3qnO98NheqaE5xu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t3qnO98NheqaE5xu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-t3qnO98NheqaE5xu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-t3qnO98NheqaE5xu .cluster text{fill:#333;}#mermaid-svg-t3qnO98NheqaE5xu .cluster span{color:#333;}#mermaid-svg-t3qnO98NheqaE5xu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-t3qnO98NheqaE5xu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t3qnO98NheqaE5xu rect.text{fill:none;stroke-width:0;}#mermaid-svg-t3qnO98NheqaE5xu .icon-shape,#mermaid-svg-t3qnO98NheqaE5xu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t3qnO98NheqaE5xu .icon-shape p,#mermaid-svg-t3qnO98NheqaE5xu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-t3qnO98NheqaE5xu .icon-shape .label rect,#mermaid-svg-t3qnO98NheqaE5xu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t3qnO98NheqaE5xu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-t3qnO98NheqaE5xu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-t3qnO98NheqaE5xu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 重启成本:高
整个作用域
重启成本:中
子树
重启成本:低
单个节点
组合阶段
Composition
布局阶段
Layout
绘制阶段
Drawing
状态读取位置
核心策略:延迟读取
状态变化时只执行
对应阶段,避免全量重组
2.2 延迟读取的实战
kotlin
// ❌ 提前读取:滚动位置变化时触发组合阶段重组
@Composable
fun BadExample(listState: LazyListState) {
val offset = listState.firstVisibleItemScrollOffset
Box(modifier = Modifier.offset(y = offset.dp)) { /* ... */ }
}
// ✅ 延迟读取:滚动位置变化只影响布局阶段
@Composable
fun GoodExample(listState: LazyListState) {
Box(
modifier = Modifier.offset {
IntOffset(0, listState.firstVisibleItemScrollOffset)
}
) { /* ... */ }
}
Modifier.offset 的lambda版本在布局阶段执行,状态读取被延迟到布局阶段。如果offset变化,只有布局阶段重新执行,不会触发组合阶段的重组。
三、重组优化策略
3.1 稳定类型:@Stable与@Immutable
Compose在判断是否跳过重组时,会检查传入参数是否"稳定"。将传递给Composable的data class标记为 @Immutable 或 @Stable,使Compose能够跳过不必要的重组。
kotlin
@Immutable
data class UserUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: AppError? = null,
)
关键原则 :@Immutable是向Compose编译器的承诺------保证类的所有属性在创建后不会改变。如果承诺被打破(用可变List并在外部修改),重组行为会变得不可预测。确认是"参数不稳定"再考虑@Immutable/@Stable,且语义必须真实,否则会出现UI不刷新。
下面是重组优化策略的决策流程图:
#mermaid-svg-hAOQNQMrkYxtgaTv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hAOQNQMrkYxtgaTv .error-icon{fill:#552222;}#mermaid-svg-hAOQNQMrkYxtgaTv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hAOQNQMrkYxtgaTv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hAOQNQMrkYxtgaTv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hAOQNQMrkYxtgaTv .marker.cross{stroke:#333333;}#mermaid-svg-hAOQNQMrkYxtgaTv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hAOQNQMrkYxtgaTv p{margin:0;}#mermaid-svg-hAOQNQMrkYxtgaTv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv .cluster-label text{fill:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv .cluster-label span{color:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv .cluster-label span p{background-color:transparent;}#mermaid-svg-hAOQNQMrkYxtgaTv .label text,#mermaid-svg-hAOQNQMrkYxtgaTv span{fill:#333;color:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv .node rect,#mermaid-svg-hAOQNQMrkYxtgaTv .node circle,#mermaid-svg-hAOQNQMrkYxtgaTv .node ellipse,#mermaid-svg-hAOQNQMrkYxtgaTv .node polygon,#mermaid-svg-hAOQNQMrkYxtgaTv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hAOQNQMrkYxtgaTv .rough-node .label text,#mermaid-svg-hAOQNQMrkYxtgaTv .node .label text,#mermaid-svg-hAOQNQMrkYxtgaTv .image-shape .label,#mermaid-svg-hAOQNQMrkYxtgaTv .icon-shape .label{text-anchor:middle;}#mermaid-svg-hAOQNQMrkYxtgaTv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hAOQNQMrkYxtgaTv .rough-node .label,#mermaid-svg-hAOQNQMrkYxtgaTv .node .label,#mermaid-svg-hAOQNQMrkYxtgaTv .image-shape .label,#mermaid-svg-hAOQNQMrkYxtgaTv .icon-shape .label{text-align:center;}#mermaid-svg-hAOQNQMrkYxtgaTv .node.clickable{cursor:pointer;}#mermaid-svg-hAOQNQMrkYxtgaTv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hAOQNQMrkYxtgaTv .arrowheadPath{fill:#333333;}#mermaid-svg-hAOQNQMrkYxtgaTv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hAOQNQMrkYxtgaTv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hAOQNQMrkYxtgaTv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hAOQNQMrkYxtgaTv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hAOQNQMrkYxtgaTv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hAOQNQMrkYxtgaTv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hAOQNQMrkYxtgaTv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hAOQNQMrkYxtgaTv .cluster text{fill:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv .cluster span{color:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hAOQNQMrkYxtgaTv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hAOQNQMrkYxtgaTv rect.text{fill:none;stroke-width:0;}#mermaid-svg-hAOQNQMrkYxtgaTv .icon-shape,#mermaid-svg-hAOQNQMrkYxtgaTv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hAOQNQMrkYxtgaTv .icon-shape p,#mermaid-svg-hAOQNQMrkYxtgaTv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hAOQNQMrkYxtgaTv .icon-shape .label rect,#mermaid-svg-hAOQNQMrkYxtgaTv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hAOQNQMrkYxtgaTv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hAOQNQMrkYxtgaTv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hAOQNQMrkYxtgaTv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
data class
Lazy列表
频繁变化状态
对象分配
Composable参数是否稳定?
Compose跳过重组
参数类型
标记@Immutable
或@Stable
使用key()
追踪条目身份
使用derivedStateOf
过滤重组
使用remember
缓存对象
确认语义真实
避免UI不刷新
避免头部插入
触发全量重组
输入频率高于
输出频率才适用
减少GC压力
3.2 key()与Lazy列表
在Lazy列表中为 items() 提供稳定的 key,帮助Compose精确追踪每个条目的身份。没有key时,Compose按位置索引区分条目,列表头部插入新条目会触发全量重组。key必须是唯一且可序列化到Bundle的类型(Int、Long、String等)。
3.3 derivedStateOf:延迟读取与重组过滤
derivedStateOf 创建一个派生状态,其值由其他状态计算得出。核心价值在于限制重组:当输入状态频繁变化,但派生结果变化频率较低时,只有派生结果变化才会触发重组。
kotlin
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
// 只有跨越索引5的边界时,才触发重组
适用条件 :输入频率必须高于输出频率。如果输入和输出的变化频率相同,使用 derivedStateOf 只是增加了额外的状态包装,反而增加了开销。
3.4 避免在重组期间分配对象
在Composable函数体中创建对象(如 SimpleDateFormat、临时List)会导致每次重组都分配新对象,增加GC压力。使用 remember 缓存:
kotlin
// ❌ 每次重组都创建新对象
val formatter = SimpleDateFormat("yyyy-MM-dd")
// ✅ 使用remember缓存
val formatter = remember { SimpleDateFormat("yyyy-MM-dd") }
四、iOS端专项性能优化
4.1 Skiko渲染管线与Android的本质差异
iOS端CMP通过Skiko(Skia for Kotlin)直接渲染到Metal,完全绕过了UIKit的布局引擎 。@Composable 树产生Skia绘制命令,编译为Metal着色器程序,渲染到 CAMetalLayer。没有UIKit布局传递,没有Core Animation隐式事务。
关键差异 :Android的OS在后台 RenderThread 预编译着色器,而iOS上每个新的着色器变体都会在主线程上触发Metal编译器。
| 维度 | Android(Pixel 7+) | iOS(iPhone 13+) |
|---|---|---|
| Skia后端 | OS集成的Skia | 捆绑的Skiko/Skia |
| 着色器编译 | 后台RenderThread | 主线程Metal编译 |
| 纹理图集内存 | OS合成器管理 | 应用级Metal分配 |
| 120fps帧预算 | 8.3ms | 8.3ms(ProMotion) |
| GPU分析工具 | Android GPU Inspector | Xcode Metal System Trace |
下面是iOS端CMP渲染管线与Android的对比图:
#mermaid-svg-Si1H0fvagJc4Wo0t{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Si1H0fvagJc4Wo0t .error-icon{fill:#552222;}#mermaid-svg-Si1H0fvagJc4Wo0t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Si1H0fvagJc4Wo0t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Si1H0fvagJc4Wo0t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Si1H0fvagJc4Wo0t .marker.cross{stroke:#333333;}#mermaid-svg-Si1H0fvagJc4Wo0t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Si1H0fvagJc4Wo0t p{margin:0;}#mermaid-svg-Si1H0fvagJc4Wo0t .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t .cluster-label text{fill:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t .cluster-label span{color:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t .cluster-label span p{background-color:transparent;}#mermaid-svg-Si1H0fvagJc4Wo0t .label text,#mermaid-svg-Si1H0fvagJc4Wo0t span{fill:#333;color:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t .node rect,#mermaid-svg-Si1H0fvagJc4Wo0t .node circle,#mermaid-svg-Si1H0fvagJc4Wo0t .node ellipse,#mermaid-svg-Si1H0fvagJc4Wo0t .node polygon,#mermaid-svg-Si1H0fvagJc4Wo0t .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Si1H0fvagJc4Wo0t .rough-node .label text,#mermaid-svg-Si1H0fvagJc4Wo0t .node .label text,#mermaid-svg-Si1H0fvagJc4Wo0t .image-shape .label,#mermaid-svg-Si1H0fvagJc4Wo0t .icon-shape .label{text-anchor:middle;}#mermaid-svg-Si1H0fvagJc4Wo0t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Si1H0fvagJc4Wo0t .rough-node .label,#mermaid-svg-Si1H0fvagJc4Wo0t .node .label,#mermaid-svg-Si1H0fvagJc4Wo0t .image-shape .label,#mermaid-svg-Si1H0fvagJc4Wo0t .icon-shape .label{text-align:center;}#mermaid-svg-Si1H0fvagJc4Wo0t .node.clickable{cursor:pointer;}#mermaid-svg-Si1H0fvagJc4Wo0t .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Si1H0fvagJc4Wo0t .arrowheadPath{fill:#333333;}#mermaid-svg-Si1H0fvagJc4Wo0t .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Si1H0fvagJc4Wo0t .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Si1H0fvagJc4Wo0t .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Si1H0fvagJc4Wo0t .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Si1H0fvagJc4Wo0t .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Si1H0fvagJc4Wo0t .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Si1H0fvagJc4Wo0t .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Si1H0fvagJc4Wo0t .cluster text{fill:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t .cluster span{color:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Si1H0fvagJc4Wo0t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Si1H0fvagJc4Wo0t rect.text{fill:none;stroke-width:0;}#mermaid-svg-Si1H0fvagJc4Wo0t .icon-shape,#mermaid-svg-Si1H0fvagJc4Wo0t .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Si1H0fvagJc4Wo0t .icon-shape p,#mermaid-svg-Si1H0fvagJc4Wo0t .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Si1H0fvagJc4Wo0t .icon-shape .label rect,#mermaid-svg-Si1H0fvagJc4Wo0t .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Si1H0fvagJc4Wo0t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Si1H0fvagJc4Wo0t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Si1H0fvagJc4Wo0t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} iOS渲染管线
Android渲染管线
相同Composable代码
后台编译
无卡顿
主线程编译
可能卡顿
Composable树
OS集成Skia
后台RenderThread
预编译着色器
GPU渲染
Composable树
Skiko/Skia
主线程Metal编译
着色器变体
CAMetalLayer
GPU渲染
4.2 Metal着色器编译停顿的诊断与修复
这是iOS端CMP最常见的卡顿来源。当Skia遇到新的绘制配置(新的混合模式、裁剪形状或渐变类型)时,Metal必须编译管线状态对象(PSO),每个变体可能阻塞数毫秒。
诊断方法:打开Xcode Instruments,附加GPU和Metal System Trace模板,关注三个信号:
MTLCreatePipelineState调用超过2ms- GPU轨道中的帧间隙,命令缓冲区提交被延迟
- Time Profiler中主线程挂起,与Skia的
GrMtlPipelineStateBuilder相关
修复方案:着色器预热。在启动画面或加载阶段强制Skia编译最常见的着色器变体。渲染一个离屏画布,执行典型的绘制操作:圆角矩形、模糊阴影、渐变填充、不同样式的文本。这会在用户看到交互内容之前预加载PSO编译。
下面是Metal着色器编译停顿的诊断与修复流程图:
#mermaid-svg-TT23YgoMD3E32tZT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TT23YgoMD3E32tZT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TT23YgoMD3E32tZT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TT23YgoMD3E32tZT .error-icon{fill:#552222;}#mermaid-svg-TT23YgoMD3E32tZT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TT23YgoMD3E32tZT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TT23YgoMD3E32tZT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TT23YgoMD3E32tZT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TT23YgoMD3E32tZT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TT23YgoMD3E32tZT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TT23YgoMD3E32tZT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TT23YgoMD3E32tZT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TT23YgoMD3E32tZT .marker.cross{stroke:#333333;}#mermaid-svg-TT23YgoMD3E32tZT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TT23YgoMD3E32tZT p{margin:0;}#mermaid-svg-TT23YgoMD3E32tZT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TT23YgoMD3E32tZT .cluster-label text{fill:#333;}#mermaid-svg-TT23YgoMD3E32tZT .cluster-label span{color:#333;}#mermaid-svg-TT23YgoMD3E32tZT .cluster-label span p{background-color:transparent;}#mermaid-svg-TT23YgoMD3E32tZT .label text,#mermaid-svg-TT23YgoMD3E32tZT span{fill:#333;color:#333;}#mermaid-svg-TT23YgoMD3E32tZT .node rect,#mermaid-svg-TT23YgoMD3E32tZT .node circle,#mermaid-svg-TT23YgoMD3E32tZT .node ellipse,#mermaid-svg-TT23YgoMD3E32tZT .node polygon,#mermaid-svg-TT23YgoMD3E32tZT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TT23YgoMD3E32tZT .rough-node .label text,#mermaid-svg-TT23YgoMD3E32tZT .node .label text,#mermaid-svg-TT23YgoMD3E32tZT .image-shape .label,#mermaid-svg-TT23YgoMD3E32tZT .icon-shape .label{text-anchor:middle;}#mermaid-svg-TT23YgoMD3E32tZT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TT23YgoMD3E32tZT .rough-node .label,#mermaid-svg-TT23YgoMD3E32tZT .node .label,#mermaid-svg-TT23YgoMD3E32tZT .image-shape .label,#mermaid-svg-TT23YgoMD3E32tZT .icon-shape .label{text-align:center;}#mermaid-svg-TT23YgoMD3E32tZT .node.clickable{cursor:pointer;}#mermaid-svg-TT23YgoMD3E32tZT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TT23YgoMD3E32tZT .arrowheadPath{fill:#333333;}#mermaid-svg-TT23YgoMD3E32tZT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TT23YgoMD3E32tZT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TT23YgoMD3E32tZT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TT23YgoMD3E32tZT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TT23YgoMD3E32tZT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TT23YgoMD3E32tZT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TT23YgoMD3E32tZT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TT23YgoMD3E32tZT .cluster text{fill:#333;}#mermaid-svg-TT23YgoMD3E32tZT .cluster span{color:#333;}#mermaid-svg-TT23YgoMD3E32tZT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TT23YgoMD3E32tZT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TT23YgoMD3E32tZT rect.text{fill:none;stroke-width:0;}#mermaid-svg-TT23YgoMD3E32tZT .icon-shape,#mermaid-svg-TT23YgoMD3E32tZT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TT23YgoMD3E32tZT .icon-shape p,#mermaid-svg-TT23YgoMD3E32tZT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TT23YgoMD3E32tZT .icon-shape .label rect,#mermaid-svg-TT23YgoMD3E32tZT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TT23YgoMD3E32tZT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TT23YgoMD3E32tZT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TT23YgoMD3E32tZT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MTLCreatePipelineState
超过2ms
GPU轨道帧间隙
命令缓冲区延迟
主线程挂起
GrMtlPipelineStateBuilder
iOS端CMP卡顿
诊断:Xcode Instruments
Metal System Trace
检查三个信号
确认着色器编译停顿
修复:着色器预热
启动画面/加载阶段
渲染离屏画布
执行典型绘制操作
圆角矩形、模糊阴影、
渐变填充、不同样式文本
预加载PSO编译
避免用户可见卡顿
4.3 纹理图集抖动的修复
如果看到每隔几秒的周期性卡顿,检查Instruments中的 Metal Resource Allocations。GPU内存分配/释放的尖峰意味着图集正在抖动------Skia的字形缓存和小图像区域被驱逐并重新上传。
修复方案:减少热路径中不同字体大小和图像缩放的种类。优先使用整数缩放的图片,使其干净地映射到图集区域。在120fps下,帧预算为8.3ms,约为60fps的一半,需要更严格地控制每帧的工作量。
4.4 Kotlin/Native GC调优
Kotlin 2.3.20为Kotlin/Native的垃圾回收器默认启用了并发标记(Concurrent Marking),使GC暂停时间更短,特别有利于Compose Multiplatform这类延迟敏感的UI应用。
对于iOS上仍有GC暂停问题的场景,可以设置 kotlin.native.binary.objcDisposeOnMain=false,让GC方法在后台线程运行,不阻塞主线程。
五、Lazy布局的性能陷阱
5.1 避免为每个列表项创建LaunchedEffect
kotlin
// ❌ 每个列表项创建LaunchedEffect,开销巨大
items(items) { item ->
LaunchedEffect(item.id) { checkIfLast(item) }
}
// ✅ 在列表级别使用一个LaunchedEffect监听listState
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
.collect { lastIndex -> if (lastIndex == items.size - 1) loadMore() }
}
5.2 避免在items()内部排序
kotlin
// ❌ 每次重组都重新排序
items(contacts.sortedWith(comparator)) { contact -> ContactRow(contact) }
// ✅ 在LazyColumn外部排序,用remember缓存
val sortedContacts = remember(contacts, comparator) {
contacts.sortedWith(comparator)
}
items(sortedContacts) { contact -> ContactRow(contact) }
5.3 AndroidView在LazyColumn中的性能问题
当 AndroidView 放置在 LazyColumn 中时,View实例将始终被丢弃并重新创建 ------即使组合层级没有变化。推荐方案是将整个列表实现为 RecyclerView,通过 AndroidView 嵌入。如果列表项数量有限,使用带 verticalScroll 的 Column 替代 LazyColumn。
六、基准测试工具链
六、基准测试工具链
6.1 Multiplatform Benchmark Framework的四种模式
JetBrains的Multiplatform Benchmark Framework提供四种测量模式,覆盖不同性能维度:
| 模式 | 描述 | 关键指标 | 适用场景 |
|---|---|---|---|
| SIMPLE | 不进行VSync同步,测量原始帧耗时 | 原始帧时长 | 评估峰值CPU/GPU渲染能力 |
| VSYNC_EMULATION | 模拟VSync行为 | 丢失帧数、CPU/GPU百分位 | 估算帧丢失和百分比 |
| REAL | 使用实际硬件VSync | FPS、实际丢失帧数 | 最准确的用户感知性能 |
| STARTUP | 捕获从进程入口到首帧的耗时 | 首帧时长 | 测量启动性能 |
6.2 find_degradation脚本:定位性能回退版本
当你将CMP从旧版本升级到新版本后,发现某个UI场景的渲染耗时明显变长,但不知道问题具体从哪个版本开始引入时,find_degradation.main.kts 脚本提供了一个针对性工具。该脚本对候选版本列表做二分查找 :以列表中的第一个版本为基线,逐个检查候选版本相对基线的耗时增幅,凡是耗时增加超过5%的判定为"出现性能回退",最终报告引入回退的首个版本及其前一个稳定版本。
脚本位于 benchmarks/multiplatform 目录下,每次对比时以 runs=3、modes=SIMPLE、frameCount=1000 的固定参数运行benchmark,确保对比口径一致。支持 macos、desktop、web 三个平台参数。
6.3 LazyGridImageView:与原生实现的量化对比
JetBrains的 LazyGridImageView 示例项目是一个现成的测试台:它将Compose Multiplatform、原生Android(Jetpack Compose)、原生iOS(SwiftUI)三种实现放在同一个项目中,渲染同一个999张图片、3列的网格,对比size、startup time、FPS、CPU/GPU usage等指标。
项目结构包含三个可运行的应用:
composeApp/:KMP + CMP实现,使用LazyVerticalGrid和Coil从Compose Multiplatform资源加载图片nativeAndroidApp/:纯Android实现,使用Jetpack Compose从Android assets加载nativeiosApp/:纯iOS实现,使用SwiftUI的AsyncImage从本地文件系统加载
三者共用同一测试场景,并提供一个"Auto Scroll"复选框,勾选后通过 animateScrollToItem 持续循环滚动网格,用于在稳定滚动状态下观察渲染表现。
七、性能分析工具速查
| 工具 | 用途 | 适用平台 | 触发时机 |
|---|---|---|---|
| Compose Compiler Reports | 不稳定参数、可跳过/可重启标志 | 全平台 | 交叉验证不稳定性问题 |
| Layout Inspector | 重组计数、跳过计数 | Android | 审计标记出热点Composable后 |
| Composition Tracing | 帧级组合时序 | Android | 警告聚集在某个屏幕时 |
| Macrobenchmark | Release回归测试 | Android | 消费级应用 |
| Xcode Instruments | GPU停滞、Metal着色器编译、纹理图集 | iOS | iOS渲染性能问题 |
| JVM Profiler | CPU/内存分析 | Desktop | 桌面端性能问题 |
| skydoves/compose-stability-analyzer | 重组时长测量、热力图 | Android/JVM | 定位重组性能问题 |
| find_degradation.main.kts | 定位性能回退版本 | macOS/Desktop/Web | 版本升级后性能下降 |
compose-stability-analyzer 0.7.4提供了 @TraceRecomposition 注解,通过 System.nanoTime() IR注入测量Composable重组时长,日志输出格式为 [Recomposition #3] UserCard (2.30ms)。
配套习题(15题)
第一组:渲染管线与重组优化(4题)
第1题 Compose三阶段渲染管线中,重启成本最高的阶段是( )
A. 绘制阶段
B. 布局阶段
C. 组合阶段
D. 三个阶段的成本相同
答案:C
解析:组合阶段的执行内容是Composable函数和状态读取,重启成本为高(整个作用域)。布局阶段成本为中(子树),绘制阶段成本为低(单个节点)。因此应将状态读取尽可能推迟到后面的阶段。
第2题 关于性能分析的构建类型,以下说法正确的是( )
A. 可以在Debug构建中进行性能分析
B. 必须在Release构建或benchmark构建类型上进行性能分析
C. Debug和Release的差异对性能分析没有影响
D. 只在Android上需要Release构建
答案:B
解析:Debug构建禁用了R8、内联和Strong Skipping,性能数据毫无意义。必须在Release构建(启用profileable)或benchmark构建类型上进行性能分析。
第3题 使用 Modifier.offset { } 的lambda版本优化性能的原理是( )
A. 代码更简洁
B. 状态读取被延迟到布局阶段,避免组合阶段重组
C. 自动缓存计算结果
D. 减少内存占用
答案:B
解析 :Modifier.offset 的lambda版本在布局阶段执行,状态读取被延迟到布局阶段。如果offset变化,只有布局阶段重新执行,不会触发组合阶段的重组。
第4题 derivedStateOf 的适用条件是( )
A. 任何状态计算都应使用
B. 输入频率高于输出频率
C. 输出频率高于输入频率
D. 输入和输出频率相同
答案:B
解析 :derivedStateOf 的适用条件是输入频率必须高于输出频率。如果输入和输出的变化频率相同,使用 derivedStateOf 只是增加了额外的状态包装,反而增加了开销。
第二组:iOS渲染优化(4题)
第5题 iOS端CMP渲染与Android端的核心差异是( )
A. iOS使用UIKit布局引擎
B. iOS通过Skiko直接渲染到Metal,着色器编译在主线程
C. Android使用OpenGL,iOS使用Vulkan
D. 两者渲染管线完全相同
答案:B
解析:iOS端CMP通过Skiko直接渲染到Metal,完全绕过UIKit布局引擎。Android的OS在后台RenderThread预编译着色器,而iOS上每个新的着色器变体都会在主线程上触发Metal编译器。
第6题 iOS端Metal着色器编译停顿的修复方案是( )
A. 在后台线程编译着色器
B. 在启动画面或加载阶段进行着色器预热
C. 禁用Skia渲染
D. 使用UIKit替代Metal
答案:B
解析:在启动画面或加载阶段强制Skia编译最常见的着色器变体。渲染一个离屏画布,执行典型的绘制操作(圆角矩形、模糊阴影、渐变填充、不同样式的文本),在用户看到交互内容之前预加载PSO编译。
第7题 iOS端纹理图集抖动的修复方案是( )
A. 增加GPU内存分配
B. 减少热路径中不同字体大小和图像缩放的种类,使用整数缩放的图片
C. 禁用纹理图集
D. 降低渲染分辨率
答案:B
解析:纹理图集抖动表现为周期性卡顿,原因是Skia的字形缓存和小图像区域被驱逐并重新上传。修复方案是减少热路径中不同字体大小和图像缩放的种类,优先使用整数缩放的图片,使其干净地映射到图集区域。
第8题 Kotlin 2.3.20为Kotlin/Native的GC带来的重要改进是( )
A. 移除了GC
B. 默认启用并发标记(Concurrent Marking)
C. 增加了GC暂停时间
D. 仅支持Android平台
答案:B
解析:Kotlin 2.3.20为Kotlin/Native的GC默认启用了并发标记,使GC暂停时间更短,特别有利于Compose Multiplatform这类延迟敏感的UI应用。
第三组:Lazy布局性能陷阱(4题)
第9题 在LazyColumn中,为每个列表项创建LaunchedEffect的问题是( )
A. 语法错误
B. 不必要的开销和对象分配,应使用列表级别的LaunchedEffect监听listState
C. 影响网络请求
D. 降低列表滚动速度
答案:B
解析 :应避免为每个列表项创建 LaunchedEffect 来检查是否为最后一项,这会导致不必要的开销和对象分配。正确的做法是在列表级别使用一个 LaunchedEffect 监听 listState。
第10题 在 items() 内部对数据做变换(如排序)的问题是( )
A. 代码不美观
B. 每次重组都重新执行变换,应在LazyColumn外部排序并用remember缓存
C. 编译错误
D. 影响网络请求
答案:B
解析 :在 items() 内部对数据做变换(如排序)会导致每次重组都重新执行变换,即使用户只是在滚动列表。应在 LazyColumn 外部排序,并用 remember 缓存结果。
第11题 当AndroidView放置在LazyColumn中时,View实例的行为是( )
A. View实例被自动复用
B. View实例将始终被丢弃并重新创建
C. View实例仅在滚动时创建
D. View实例不会被创建
答案:B
解析:当AndroidView放置在LazyColumn等可复用容器中时,View实例将始终被丢弃并重新创建------即使组合层级没有变化。推荐方案是将整个列表实现为RecyclerView,通过AndroidView嵌入。
第12题 以下哪种做法可以避免在重组期间分配对象?( )
A. 直接创建对象
B. 使用 remember 缓存
C. 使用全局变量
D. 使用反射
答案:B
解析 :在Composable函数体中创建对象会导致每次重组都分配新对象,增加GC压力。应使用 remember 缓存对象,避免在重组期间重复分配。
第四组:基准测试与工具链(3题)
第13题 Multiplatform Benchmark Framework的四种模式不包括( )
A. SIMPLE
B. VSYNC_EMULATION
C. RANDOM
D. STARTUP
答案:C
解析:四种模式是SIMPLE(不进行VSync同步,测量原始帧耗时)、VSYNC_EMULATION(模拟VSync行为)、REAL(使用实际硬件VSync)、STARTUP(捕获从进程入口到首帧的耗时)。
第14题 find_degradation脚本判定性能回退的标准是( )
A. 耗时增加超过1%
B. 耗时增加超过5%
C. 耗时增加超过10%
D. 耗时增加超过50%
答案:B
解析:该脚本对候选版本列表做二分查找:以列表中的第一个版本为基线,逐个检查候选版本相对基线的耗时增幅,凡是耗时增加超过5%的判定为"出现性能回退"。
第15题 LazyGridImageView示例项目用于对比的三种实现不包括( )
A. Compose Multiplatform
B. 原生Android(Jetpack Compose)
C. 原生iOS(SwiftUI)
D. Flutter
答案:D
解析:LazyGridImageView示例项目将Compose Multiplatform、原生Android(Jetpack Compose)、原生iOS(SwiftUI)三种实现放在同一个项目中,渲染同一个999张图片、3列的网格,对比size、startup time、FPS、CPU/GPU usage等指标。Flutter不在对比范围内。
本课知识点总结
1. 性能优化的第一原则
绝不在Debug构建中做性能分析------Debug禁用了R8、内联和Strong Skipping。必须在Release构建或benchmark构建类型上进行性能分析。性能优化分为三个维度:重组优化、渲染优化、内存优化。
2. 三阶段渲染管线与延迟读取
Compose每一帧经过组合(成本高、重启范围大)、布局(成本中)、绘制(成本低)三个阶段。将状态读取尽可能推迟到后面的阶段 ------在Composable函数体中读取状态触发组合阶段重组,在measure lambda中读取触发布局阶段,在draw/graphicsLayer lambda中读取仅触发绘制阶段。Modifier.offset { } 的lambda版本是延迟读取的典型应用。
3. 重组优化策略
@Immutable/@Stable 标记稳定类型帮助Compose跳过重组;key() 在Lazy列表中精确追踪条目身份;derivedStateOf 在输入频率高于输出频率时过滤重组;remember 缓存对象避免重组期间分配。
4. iOS端专项优化
iOS通过Skiko直接渲染到Metal,着色器编译在主线程 是最大的卡顿来源。修复方案包括:着色器预热 (启动画面渲染离屏画布执行典型绘制操作)、纹理图集抖动修复 (减少字体大小和图像缩放种类,使用整数缩放图片)、Kotlin/Native GC调优 (Kotlin 2.3.20默认启用并发标记,可设置 objcDisposeOnMain=false 让GC在后台线程运行)。120fps下帧预算仅8.3ms。
5. Lazy布局性能陷阱
避免为每个列表项创建LaunchedEffect;避免在 items() 内部排序;AndroidView在LazyColumn中View实例将始终被丢弃并重新创建;使用 remember 缓存变换结果。
6. 基准测试工具链
Multiplatform Benchmark Framework 提供四种模式:SIMPLE(原始帧耗时)、VSYNC_EMULATION(丢失帧数)、REAL(实际FPS)、STARTUP(启动耗时)。find_degradation.main.kts 通过二分查找定位性能回退版本,耗时增加超过5%判定为回退。LazyGridImageView 对比CMP与原生Android/iOS的size、startup time、FPS、CPU/GPU usage。compose-stability-analyzer 提供重组时长测量。Compose Compiler Reports 检测不稳定参数和可跳过标志。
下一课预告:第18课将深入测试策略与质量保障,包括Composable UI测试的跨平台方案、ViewModel与业务逻辑的单元测试(commonTest的编写)、平台特定测试(Android的Instrumented Test与iOS的XCTest)、测试中的依赖替换(Koin测试模块、Mock引擎)、语义树在UI测试中的应用,以及CI/CD中的测试自动化。