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。

修复建议

  1. 在面板联动动画期间对 SharedNotificationContainerKeyguardBottomAreaView 调用 setForceSDFT(true)(已有通知 Row 的先例)
  2. AllInOne 时钟动画期间为 KeyguardClockContainer 设置 LAYER_TYPE_HARDWARE,避免可变字体参数变化导致的每帧重光栅化
  3. 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 heads123
总耗时39.47ms
高耗时帧(>500us)52
Atlas full 事件0
高耗时窗口432ms
趋势递减(前半段高于后半段)

耗时分布

区间帧数
<10us20
10-50us18
50-100us28
100-200us5
200-500us0
>500us52

STEP 2: TOP View 定位

View出现次数总耗时(ms)平均(us)占比
KeyguardIndicationTextView5720.90336753%
TextView (通知)4016.90842343%
MiuiClock200.31216<1%
ClassicTextAreaView30.17558<1%

当前帧 (4267789) 的 AtlasTextOp 实例

opid阶段耗时来源 View
2711prepare606usTextView (HybridNotificationView/通知正文)
2684prepare144usClassicTextAreaView (锁屏分钟时钟)
2694prepare64usKeyguardIndicationTextView (锁屏底部提示)

STEP 3: Op 合并分析

指标数值
总录制 Op 数295
Chain heads123
Atlas slices246
合并率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:4473doScale(notificationContainer, keyguardInfoLayerScale)
scale 计算KeyguardPanelViewController.kt:4451val scale = 1 - afterFrictionValue(1 - fraction, 1f) * 0.3f
doScale 实现KeyguardPanelViewController.kt:3275-3279view.scaleX = scale; view.scaleY = scale
hasOverlappingRendering=falseSharedNotificationContainerBinder.kt:287forceNoOverlappingRendering = 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 也做 scaleKeyguardPanelViewController.kt:4495doScaleAndAlpha(keyguardClockInjector.getView(), if (canScale()) scale else 1f, infoAlpha)
alpha 触发 saveLayerTrace 数据alpha=0.995287, 两个 1728x2368 GPU 纹理分配
可变字体动画AllInOneClockAnimation.kt:458FONT_WEIGHT animated as ValueProperty
Texture uploadsTrace 数据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:4471doScale(keyguardBottomAreaInjector.bottomView, keyguardInfoLayerScale)
Legacy AnimationKeyguardIndicationInjector.java:172-176new TranslateAnimation(SELF, 0, SELF, 0, SELF, 2, SELF, 0)
sk_relax 跳变知识库文档22scale 每帧递减 > 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 调用链验证 ✅

  • updateKeyguardElementsExpansionInternaldoScale(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()=falsepromotedToLayer()=false → 不自动升级为 layer ✅
  • setForceSDFT 缺失:搜索确认只有 MiuiNotificationPanelAnimControllerImpl.kt 中对 notification row 调用了 setForceSDFT2,SharedNotificationContainer 和 KeyguardBottomAreaView 均未调用 ✅

5.4 数据交叉验证 ✅

验证项预期实际结论
高耗时窗口 vs 动画时长面板联动 ~500ms432ms✅ 吻合(DecelerateInterpolator 尾部趋近终值)
高耗时帧数 vs 120Hz500ms / 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.ktKeyguardClockContainer.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 + KeyguardIndicationTextView37.8ms/100帧<1ms97%
方案 2ClassicTextAreaView0.175ms/100帧~0ms100%
方案 3KeyguardIndicationTextView(已含方案1)消除 invalidate辅助

总计:100帧内 AtlasTextOp 总耗时从 39.47ms → <2ms,每帧平均节省 0.37ms,峰值帧节省 0.6-1.0ms