AtlasTextOp 分析报告: DrawFrames 4267789
结论
现象:DrawFrames 4267789 帧耗时 7.979ms,其中 AtlasTextOp prepare 累计耗时 814us(opid:2711=606us + opid:2684=144us + opid:2694=64us),占帧总耗时 10.2%。100帧窗口内 AtlasTextOp 总耗时 39.47ms,高耗时帧 52 帧,集中在 432ms 动画窗口。
根因:
- 通知 TextView (opid:2711, 606us):面板联动动画对 SharedNotificationContainer 执行
doScale(scale),scale 为小数值(~0.97),导致子 View 文字的 Skia Strike Descriptor 每帧变化,触发全量字形重光栅化。hasOverlappingRendering=false(KEYGUARD 状态)导致无 HW Layer 保护。 - 时钟 ClassicTextAreaView (opid:2684, 144us):AllInOne 时钟的可变字体动画(MiSans wght/wdth/hght 轴)每帧改变 font variation 参数,导致 Strike Key 每帧不同;同时 KeyguardClockContainer alpha=0.995287 触发 saveLayer。
- KeyguardIndicationTextView (opid:2694, 64us):KeyguardBottomAreaView 同样受
doScale()影响 + Legacy TranslateAnimation 导致每帧 invalidate。
修复建议:
- 在面板联动动画期间对
SharedNotificationContainer和KeyguardBottomAreaView调用setForceSDFT(true)(已有通知 Row 的先例) - AllInOne 时钟动画期间为
KeyguardClockContainer设置LAYER_TYPE_HARDWARE,避免可变字体参数变化导致的每帧重光栅化 - 将
KeyguardIndicationTextView的 Legacy TranslateAnimation 替换为 ObjectAnimator
预期收益:AtlasTextOp 总耗时从 39.47ms 降至 <2ms(减少 95%+),每帧节省 0.6-1.0ms
基本信息
| 项目 | 值 |
|---|---|
| Trace 文件 | perfetto_1780036731.pftrace |
| 帧编号 | 4267789 (vsyncId) |
| 进程 | com.android.systemui (PID: 8543) |
| 线程 | RenderThread (TID: 8631) |
| 帧总耗时 | 7.979 ms |
| 场景 | 锁屏面板上滑联动动画 |
| 分析范围 | 100帧, 1099ms |
STEP 1: 帧级统计
| 指标 | 数值 |
|---|---|
| 总帧数 | 100 |
| AtlasTextOp 总 slice 数 | 246 |
| Chain heads | 123 |
| 总耗时 | 39.47ms |
| 高耗时帧(>500us) | 52 |
| Atlas full 事件 | 0 |
| 高耗时窗口 | 432ms |
| 趋势 | 递减(前半段高于后半段) |
耗时分布
| 区间 | 帧数 |
|---|---|
| <10us | 20 |
| 10-50us | 18 |
| 50-100us | 28 |
| 100-200us | 5 |
| 200-500us | 0 |
| >500us | 52 |
STEP 2: TOP View 定位
| View | 出现次数 | 总耗时(ms) | 平均(us) | 占比 |
|---|---|---|---|---|
| KeyguardIndicationTextView | 57 | 20.903 | 367 | 53% |
| TextView (通知) | 40 | 16.908 | 423 | 43% |
| MiuiClock | 20 | 0.312 | 16 | <1% |
| ClassicTextAreaView | 3 | 0.175 | 58 | <1% |
当前帧 (4267789) 的 AtlasTextOp 实例
| opid | 阶段 | 耗时 | 来源 View |
|---|---|---|---|
| 2711 | prepare | 606us | TextView (HybridNotificationView/通知正文) |
| 2684 | prepare | 144us | ClassicTextAreaView (锁屏分钟时钟) |
| 2694 | prepare | 64us | KeyguardIndicationTextView (锁屏底部提示) |
STEP 3: Op 合并分析
| 指标 | 数值 |
|---|---|
| 总录制 Op 数 | 295 |
| Chain heads | 123 |
| Atlas slices | 246 |
| 合并率 | 58.3% |
| SubRun 展开 | 2.0x |
| 诊断 | 合并率正常,问题在 prepare 阶段 |
STEP 4: 源码根因分析
4.1 通知 TextView (opid:2711, 606us) — SharedNotificationContainer scale 动画
根因判定:父容器 scale 动画导致 Strike Descriptor 每帧变化
触发路径:
面板上滑触摸事件
→ PanelViewController.setExpandedHeight()
→ updateExpansionAndVisibility()
→ KeyguardPanelViewController.updateKeyguardElementsExpansion(false)
→ updateKeyguardElementsExpansionInternal(fraction) [line 4446]
→ scale = 1 - afterFrictionValue(1-fraction, 1f) * 0.3f [line 4451]
→ doScale(notificationContainer, keyguardInfoLayerScale) [line 4473]
→ SharedNotificationContainer.scaleX = scale (e.g., 0.97) [line 3277]
→ SharedNotificationContainer.scaleY = scale [line 3278]
渲染路径:
RenderThread 渲染 SharedNotificationContainer
→ hasOverlappingRendering=false (KEYGUARD 状态) [SharedNotificationContainerBinder.kt:287]
→ 无 HW Layer → scale 直接写入 Canvas CTM
→ Replay 子 View display list
→ HybridNotificationView.TextView 的 drawTextBlob
→ positionMatrix 包含祖先的 0.97x scale
→ VertexFiller::CanUseDirect() 精确比较 scale 失败 [VertexFiller.cpp:98]
→ SubRun 重建 → MakeMask(positionMatrix) [SubRunContainer.cpp:1530]
→ rec.fPost2x2[0][0] = sk_relax(0.97) [SkScalerContext.cpp:1106]
→ Strike Descriptor 与缓存不匹配 → 全量字形重光栅化
→ ~50个字形 × ~12us/字形 ≈ 606us
关键证据:
| 证据 | 文件:行号 | 内容 |
|---|---|---|
| doScale 调用 | KeyguardPanelViewController.kt:4473 | doScale(notificationContainer, keyguardInfoLayerScale) |
| scale 计算 | KeyguardPanelViewController.kt:4451 | val scale = 1 - afterFrictionValue(1 - fraction, 1f) * 0.3f |
| doScale 实现 | KeyguardPanelViewController.kt:3275-3279 | view.scaleX = scale; view.scaleY = scale |
| hasOverlappingRendering=false | SharedNotificationContainerBinder.kt:287 | forceNoOverlappingRendering = state == StatusBarState.KEYGUARD |
| 阻尼公式 | KeyguardPanelViewController.kt:4499-4502 | (per³/3 - per² + per) * range |
setForceSDFT 缺失对比:
- ExpandableNotificationRow:有
setForceSDFT2(view, true)保护 (MiuiNotificationPanelAnimControllerImpl.kt:320) - SharedNotificationContainer:无保护
- KeyguardBottomAreaView:无保护
4.2 时钟 ClassicTextAreaView (opid:2684, 144us) — 可变字体动画
根因判定:AllInOne 时钟可变字体 (wght/wdth/hght) 动画 + alpha saveLayer
触发路径:
面板上滑联动
→ KeyguardPanelViewController.updateKeyguardElementsExpansionInternal()
→ doScaleAndAlpha(keyguardClockInjector.getView(), scale, infoAlpha) [line 4495]
→ KeyguardClockContainer.alpha = 0.995287
→ promotedToLayer() → saveLayerAlpha (因 alpha<1 + hasOverlappingRendering=true)
→ 子树渲染到 offscreen layer
同时, AllInOneClockAnimation (Folme spring 动画) 正在运行:
→ folmeClockListener.onUpdate() [AllInOneClockAnimation.kt:47-98]
→ updateClockStyle(timeWidth, timeHeight, timeWeight, ...) [line 386-407]
→ AllInOneBase.setAllInOneClockRectParams() [line 2335]
→ applyAllInOneRect() → setClockViewRect() [line 2428]
→ VariableFontQueryUtils.query() → 计算新的 wght, wdth, hght [line 2462]
→ TextMeasureUtils.preparePaint(paint, textSizePx, wght, wdth, hght) [line 2493]
→ fontVariationSettings 变化
→ TimeView.onDraw() → handleParams() → applyFontVariation() [TimeView.java:178]
→ Strike Descriptor 变化 (含 fontVariationSettings) → 字形重光栅化
关键证据:
| 证据 | 文件:行号 | 内容 |
|---|---|---|
| Clock 也做 scale | KeyguardPanelViewController.kt:4495 | doScaleAndAlpha(keyguardClockInjector.getView(), if (canScale()) scale else 1f, infoAlpha) |
| alpha 触发 saveLayer | Trace 数据 | alpha=0.995287, 两个 1728x2368 GPU 纹理分配 |
| 可变字体动画 | AllInOneClockAnimation.kt:458 | FONT_WEIGHT animated as ValueProperty |
| Texture uploads | Trace 数据 | 512x140 + 416x40 = 字形 atlas 上传 |
为什么是 144us 而非 600us:时钟只有数字 “0-9” + ”:” 约 12 个字形,远少于通知文本的 ~50 个 CJK 字形。
4.3 KeyguardIndicationTextView (opid:2694, 64us) — scale + Legacy Animation 双重打击
根因判定:父容器 scale 动画 + Legacy TranslateAnimation 导致每帧 DL 重建
触发路径:
路径 1 (scale):
updateKeyguardElementsExpansionInternal()
→ doScale(keyguardBottomAreaInjector.bottomView, keyguardInfoLayerScale) [line 4471]
→ KeyguardBottomAreaView.scaleX = 0.999636
→ CTM scale 传递给子 View → Strike Descriptor 变化 (sk_relax 量化跳变)
路径 2 (Legacy Animation):
KeyguardIndicationInjector.handlePowerIndicationAnimation()
→ TranslateAnimation(SELF 2→0, 500ms) [line 172-176]
→ 每帧 parent.invalidate() → Display List 重建 → 新 DrawTextBlob Op
关键证据:
| 证据 | 文件:行号 | 内容 |
|---|---|---|
| doScale 调用 | KeyguardPanelViewController.kt:4471 | doScale(keyguardBottomAreaInjector.bottomView, keyguardInfoLayerScale) |
| Legacy Animation | KeyguardIndicationInjector.java:172-176 | new TranslateAnimation(SELF, 0, SELF, 0, SELF, 2, SELF, 0) |
| sk_relax 跳变 | 知识库文档22 | scale 每帧递减 > 1/1024 时每帧 Strike miss |
为什么只有 64us:这一帧中 KeyguardIndicationTextView 的字形可能部分命中了已有 Strike(因为 sk_relax 量化后仍为 1.0),或者该帧文字较少(“充电中” 仅 3 个 CJK 字形)。
STEP 5: 证据链核对
5.1 文件/行号验证 ✅
| 引用 | 验证结果 |
|---|---|
| KeyguardPanelViewController.kt:4451 (scale计算) | ✅ 确认存在 |
| KeyguardPanelViewController.kt:4473 (doScale notificationContainer) | ✅ 确认存在 |
| KeyguardPanelViewController.kt:3275-3279 (doScale实现) | ✅ 确认 view.scaleX = scale; view.scaleY = scale |
| SharedNotificationContainerBinder.kt:287 (forceNoOverlappingRendering) | ✅ 确认 = state == StatusBarState.KEYGUARD |
| SharedNotificationContainer.kt:131-132 (hasOverlappingRendering) | ✅ 确认 !injector.forceNoOverlappingRendering && super.hasOverlappingRendering() |
| MiuiNotificationPanelAnimControllerImpl.kt:320 (setForceSDFT2) | ✅ 确认只对 notification row view 调用 |
| KeyguardPanelViewController.kt:4495 (clock doScaleAndAlpha) | ✅ 确认 if (canScale()) scale else 1f |
5.2 调用链验证 ✅
updateKeyguardElementsExpansionInternal→doScale(notificationContainer, ...)→view.scaleX = scale✅doScale不触发 child invalidate,只更新 RenderNode property ✅hasOverlappingRendering=false导致无 HW Layer → scale 嵌入 CTM → Skia 看到分数 scale ✅
5.3 机制验证 ✅
- Strike miss 机制:
sk_relax(0.97)≠sk_relax(1.0)→ fPost2x2 不同 → Strike Descriptor 不匹配 → 全量字形重光栅化 ✅ - 无 HW Layer 原因:
forceNoOverlappingRendering=true(KEYGUARD) →hasOverlappingRendering()=false→promotedToLayer()=false→ 不自动升级为 layer ✅ - setForceSDFT 缺失:搜索确认只有
MiuiNotificationPanelAnimControllerImpl.kt中对 notification row 调用了setForceSDFT2,SharedNotificationContainer 和 KeyguardBottomAreaView 均未调用 ✅
5.4 数据交叉验证 ✅
| 验证项 | 预期 | 实际 | 结论 |
|---|---|---|---|
| 高耗时窗口 vs 动画时长 | 面板联动 ~500ms | 432ms | ✅ 吻合(DecelerateInterpolator 尾部趋近终值) |
| 高耗时帧数 vs 120Hz | 500ms / 8.33ms ≈ 60帧 | 52帧 | ✅ 合理(部分帧 sk_relax 未跳变) |
| 递减趋势 | 缓动曲线导致 scale 变化率递减 → 后期缓存命中增加 | 前半段高后半段低 | ✅ 一致 |
| Atlas full = 0 | 问题不是空间不足 | 确认 0 | ✅ 排除 Atlas 驱逐 |
修复方案
方案 1(高优):为 SharedNotificationContainer 和 KeyguardBottomAreaView 添加 setForceSDFT
修改文件:KeyguardPanelViewController.kt
在面板联动动画开始时(updateKeyguardElementsExpansionInternal 首次被调用时)对相关 View 开启 forceSDFT,动画结束后关闭:
// 在 updateKeyguardElementsExpansionInternal 中:
if (scale != 1f && !isForceSDFTEnabled) {
NotificationUtil.setForceSDFT2(notificationContainer, true)
NotificationUtil.setForceSDFT2(keyguardBottomAreaInjector.bottomView, true)
isForceSDFTEnabled = true
} else if (scale == 1f && isForceSDFTEnabled) {
NotificationUtil.setForceSDFT2(notificationContainer, false)
NotificationUtil.setForceSDFT2(keyguardBottomAreaInjector.bottomView, false)
isForceSDFTEnabled = false
}预期效果:Strike Cache 不再受 scale 影响,AtlasTextOp 从 600us/帧 降至 <5us/帧
风险:低 — SDF 在缩放范围 0.91.0 内视觉质量与 DirectMask 几乎无差异
方案 2(中优):时钟可变字体动画期间使用 HW Layer
修改文件:AllInOneClockAnimation.kt 或 KeyguardClockContainer.kt
// 动画开始时
keyguardClockContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// 动画结束时
keyguardClockContainer.setLayerType(View.LAYER_TYPE_NONE, null)预期效果:时钟 AtlasTextOp 从 144us 降至 0(layer 缓存复用) 风险:中 — 增加 ~34MB GPU 内存占用(两个 1728x2368 纹理),需评估设备内存压力
方案 3(低优):替换 KeyguardIndicationTextView 的 Legacy Animation
修改文件:KeyguardIndicationInjector.java
// 替换 TranslateAnimation 为 ObjectAnimator
ObjectAnimator translateAni = ObjectAnimator.ofFloat(
indicationTextView, View.TRANSLATION_Y,
indicationTextView.getHeight() * 2f, 0f);
translateAni.setDuration(500);
ObjectAnimator alphaAni = ObjectAnimator.ofFloat(
indicationTextView, View.ALPHA, 0f, 1f);
alphaAni.setDuration(500);
AnimatorSet set = new AnimatorSet();
set.playTogether(translateAni, alphaAni);
set.start();预期效果:消除 Legacy Animation 导致的每帧 invalidate 风险:低 — Property Animation 是标准替代方案
整体收益评估
| 方案 | 目标 View | 当前耗时 | 优化后 | 节省 |
|---|---|---|---|---|
| 方案 1 | 通知 TextView + KeyguardIndicationTextView | 37.8ms/100帧 | <1ms | 97% |
| 方案 2 | ClassicTextAreaView | 0.175ms/100帧 | ~0ms | 100% |
| 方案 3 | KeyguardIndicationTextView | (已含方案1) | 消除 invalidate | 辅助 |
总计:100帧内 AtlasTextOp 总耗时从 39.47ms → <2ms,每帧平均节省 0.37ms,峰值帧节省 0.6-1.0ms。