为什么 TimeView 和 ClassicTextAreaView 没有 AtlasTextOp 耗时问题

核心结论

在同一个锁屏上划动画中,时钟区域(AllInOneMinuteClock)和锁屏底部文字区域(KeyguardIndicationTextView)都被父级 scale + alpha 动画影响,但 trace 数据表明两者表现天壤之别:

View高耗时帧数AtlasTextOp 总耗时结论
TimeView (hour_view,时钟大数字)0 帧0ms完全不走 AtlasTextOp
ClassicTextAreaView (text_area,日期文字)1 帧0.146ms仅首帧,后续帧被 layer 缓存
KeyguardIndicationTextView (锁屏提示文字)55 帧20.9ms每帧 cache miss

原因各不相同

  • TimeView:继承自 View(非 TextView),使用 Canvas.drawPath() 绘制文字轮廓,根本不经过 glyph atlas 路径
  • ClassicTextAreaView:继承自 TextView,确实走 AtlasTextOp,但其父容器 KeyguardClockContainerhasOverlappingRendering=true(默认)在 alpha<1 时形成 persistent hardware layer,layer 内容只首帧绘制一次

背景:共同的动画驱动

上划解锁时,KeyguardPanelViewController.updateKeyguardElementsExpansionInternal() 对所有锁屏元素施加 scale + alpha 动画:

// KeyguardPanelViewController.kt:4471-4472
keyguardBottomAreaInjector.bottomView?.transitionAlpha = bottomViewAlpha  // alpha < 1
doScale(keyguardBottomAreaInjector.bottomView, keyguardInfoLayerScale)    // scale = 0.999636
 
// KeyguardPanelViewController.kt:4495-4496
doScaleAndAlpha(keyguardClockInjector.getView(), scale, infoAlpha)        // 时钟也同样!
doScaleAndAlpha(keyguardClockInjector.getSecondaryClockLayerView(), scale, infoAlpha)

View 层级:

HyperOSKeyguardRootView
├── KeyguardClockContainer                    ← doScaleAndAlpha(scale, alpha)
│   └── FrameLayout
│       └── AllInOneMinuteClock
│           ├── TimeView (hour_view)          ← 时钟大数字 "10:30"
│           └── ClassicTextAreaView (text_area) ← 日期 "周一 6月2日"
│
└── KeyguardBottomAreaView                    ← doScale + transitionAlpha
    └── LinearLayout
        ├── KeyguardIndicationTextView        ← "上滑解锁" 每帧 ~600μs
        └── KeyguardIndicationTextView        ← 充电状态文字

两个区域受到相同的 scale + alpha 动画驱动。但表现完全不同。


TimeView (hour_view):为什么完全不走 AtlasTextOp

根因:使用 Path 绘制而非 drawText

文件common_widgets/miuiclock/src/main/java/com/miui/clock/allInOne/TimeView.kt

class TimeView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {   // ★ 继承 View,不是 TextView ★
 
    private val mTextPath = Path()
    private val mColonPath = Path()
 
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // ...
        canvas.drawPath(mTextPath, textPaint)      // ★ 绘制文字轮廓路径 ★
        canvas.drawPath(mColonPath, textPaint)     // ★ 绘制冒号路径 ★
    }
}

为什么 drawPath 不走 AtlasTextOp

在 Skia/HWUI 渲染管线中,文字绘制有两条完全不同的路径:

路径 A: drawText / drawTextBlob(文字光栅化路径)
─────────────────────────────────────────────
TextView.onDraw()
  → Canvas.drawText(text, x, y, paint)
    → SkCanvas::drawTextBlob()
      → TextBlobRedrawCoordinator::drawForMaskType()
        → SubRunContainer::MakeInAlloc()
          → prepare_for_direct_mask_drawing()
            → SkPackedGlyphID{glyphID, mappedPos, mask}  ← 含亚像素位置!
              → AtlasTextOp::onPrepare() → 光栅化字形位图
                → 受 scale 亚像素偏移影响 → cache miss

路径 B: drawPath(几何路径渲染路径)
─────────────────────────────────────────────
TimeView.onDraw()
  → Canvas.drawPath(mTextPath, paint)
    → SkCanvas::drawPath()
      → PathOp / FillRectOp / tessellation
        → GPU 顶点着色器直接变换几何体
          → 无 glyph atlas、无亚像素定位、无 cache key
            → ★ 完全不涉及 AtlasTextOp ★

Path 绘制为什么不受 scale 影响

Path 存储的是文字的矢量轮廓(贝塞尔曲线控制点),不是像素位图。当父级 scale 变化时:

  1. display list 中录制的是 Path 的几何数据(控制点坐标)
  2. 回放时 canvas CTM 含父级 scale
  3. GPU 顶点着色器直接对控制点做矩阵变换
  4. 光栅化在变换后的坐标空间进行 — 不需要 atlas 缓存

对比字形光栅化:

  • 字形位图是在特定亚像素位置预渲染的像素数据
  • 位置变化 = 需要重新光栅化
  • Path 是位置无关的几何描述 = 任何位置都直接变换

性能特征

指标drawPath (TimeView)drawText (TextView)
首帧开销Path tessellation(GPU 三角化)~0.1ms字形光栅化 + atlas 上传 ~0.6ms
后续帧 scale 变化GPU 顶点变换 ~0.001ms亚像素 miss → 重新光栅化 ~0.6ms
是否涉及 atlas❌ 不涉及✅ 依赖 glyph atlas
Cache key 含亚像素❌ 不适用(glyphID, subPixelX, subPixelY)

ClassicTextAreaView (text_area):为什么仅首帧有 AtlasTextOp

根因:父容器形成 Persistent Hardware Layer

文件common_widgets/miuiclock/src/main/java/com/miui/clock/classic/ClassicTextAreaView.java

public class ClassicTextAreaView extends TextView {  // ★ 继承 TextView ★
 
    @Override
    protected void onDraw(Canvas canvas) {
        // ...
        canvas.drawText(text, currentX, baseLineY, paint);     // ★ 走 drawText → AtlasTextOp ★
        canvas.drawText(dateForeground, dateLeft, baseLineY, paint);
    }
}

ClassicTextAreaView 确实使用 canvas.drawText(),会走 AtlasTextOp 路径。但它只在第 1 帧出现高耗时(0.146ms),后续帧完全没有 AtlasTextOp。

Persistent Hardware Layer 的保护机制

ClassicTextAreaView 位于 KeyguardClockContainer 子树下。动画期间:

KeyguardClockContainer (FrameLayout)
  transitionAlpha = 0.99... (< 1.0)        ← doScaleAndAlpha 设置
  hasOverlappingRendering = true            ← FrameLayout 默认值
  → promotedToLayer() = true               ← alpha<1 && overlapping=true
  → effectiveLayerType() = RenderLayer     ← 被提升为 persistent layer!

HWUI 判定逻辑RenderProperties.h:649-655):

bool promotedToLayer() const {
    // ...
    if (!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 &&
        mPrimitiveFields.mHasOverlappingRendering) {
        return true;   // ★ 时钟容器满足此条件 ★
    }
    return false;
}

一旦成为 persistent layer:

首帧:Layer 创建并绘制内容

prepareTree:
  KeyguardClockContainer.pushLayerUpdate()
    → effectiveLayerType() = RenderLayer
    → createOrUpdateLayer(this)            ← 创建 layer surface(GPU 纹理)
    → enqueueLayerWithDamage(this, dirty)  ← 加入重绘队列

renderLayers:
  ATRACE_FORMAT("drawLayer [FrameLayout] %.1f x %.1f")   ← trace 中看到的!
  renderLayerImpl(clockContainerNode):
    → layerCanvas = layerNode->getLayerSurface()->getCanvas()
    → layerCanvas->clear(SK_ColorTRANSPARENT)
    → root.forceDraw(layerCanvas)
      → 回放 display list(包含 ClassicTextAreaView 的 drawText)
        → AtlasTextOp::onPrepare() → 光栅化字形 → 0.146ms
    → layer 内容绘制完毕,缓存为 GPU 纹理

后续帧:Layer 内容不 dirty,直接 composite 纹理

prepareTree:
  KeyguardClockContainer.prepareLayer()       [RenderNode.cpp:281-306]
    → layerType = effectiveLayerType() = RenderLayer
    → info.damageAccumulator->popTransform()   ← ★ 重置 damage ★
    → info.damageAccumulator->pushTransform(this)
    → if (dirtyMask & DISPLAY_LIST) {          ← display list 未变 → 条件不满足
          damageSelf(info);                    ← 不执行!
      }
    // layer 内容 damage 为空 → 不加入重绘队列

  KeyguardClockContainer.pushLayerUpdate()    [RenderNode.cpp:342-344]
    → info.damageAccumulator->peekAtDirty(&dirty)
    → dirty.isEmpty() == true                  ← ★ 无 dirty → 不重绘 ★
    → enqueueLayerWithDamage(this, empty_dirty)  ← dirty 为空

renderLayers:
  → dirty 为空 → 跳过此 layer 的重绘

drawContent (composite 阶段):
  if (renderNode->getLayerSurface() && mComposeLayer) {  [RenderNodeDrawable.cpp:425]
      // ★ 直接拿 layer 纹理做 composite ★
      SkPaint paint;
      layerNeedsPaint(layerProperties, alphaMultiplier, &paint);  // 设置 alpha
      snapshotImage = renderNode->getLayerSurface()->makeImageSnapshot();
      canvas->drawImageRect(snapshotImage, ..., paint);
      // GPU 对纹理做 scale + alpha 变换
      // ★ 不重放 display list → 不执行 drawText → 不触发 AtlasTextOp ★
  }

关键代码:prepareLayer 重置 damage

文件frameworks/base/libs/hwui/RenderNode.cpp:281-306

void RenderNode::prepareLayer(TreeInfo& info, uint32_t dirtyMask) {
    LayerType layerType = properties().effectiveLayerType();
    if (CC_UNLIKELY(layerType == LayerType::RenderLayer)) {
        // ★ 重置 damage accumulator ★
        // 到目前为止的 damage(父级 scale 变化)传递给 parent 影响屏幕重绘
        // 但不影响 layer 自身内容
        info.damageAccumulator->popTransform();
        info.damageAccumulator->pushTransform(this);
 
        // ★ 只有 DISPLAY_LIST 变化才 damage layer 内容 ★
        if (dirtyMask & DISPLAY_LIST) {
            damageSelf(info);  // 属性变化(scale/alpha)不走这里!
        }
    }
}

属性变化(scale/alpha)设置的是 GENERIC dirty flag,不是 DISPLAY_LIST。所以 layer 内容不被 damage。

性能特征

阶段首帧后续帧
Layer 内容绘制一次(含 drawText → AtlasTextOp 0.146ms)不重绘(内容缓存为纹理)
Scale 变化-GPU 纹理缩放(顶点变换)~0.001ms
Alpha 变化-GPU 纹理混合(paint alpha)~0.001ms
总开销0.146ms(一次性)≈0ms

对比:为什么 KeyguardIndicationTextView 每帧都有问题

根因:父容器 hasOverlappingRendering = false 阻止了 layer 形成

文件packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt:47-49

class KeyguardBottomAreaView : EffectivelyVisibleFlowLayout(...) {
    override fun hasOverlappingRendering(): Boolean {
        return false   // ★ MIUI 显式设为 false ★
    }
}

导致 promotedToLayer() 条件不满足:

// RenderProperties.h:649-655
if (!MathUtils::isZero(mAlpha) && mAlpha < 1 &&
    mHasOverlappingRendering) {   // ← false!条件不满足
    return true;
}
// → promotedToLayer() = false
// → 不形成 persistent layer

不形成 layer 后,RenderNodeDrawable::drawContent() 走 else 分支:

// RenderNodeDrawable.cpp:639-667
} else {
    if (alphaMultiplier < 1.0f) {
        // ★ AlphaFilterCanvas 路径 — 每帧重放 display list ★
        AlphaFilterCanvas alphaCanvas(canvas, alphaMultiplier);
        displayList->draw(&alphaCanvas);
        // → canvas CTM 含父级 scale
        // → drawTextBlob 的 mappedPos 亚像素每帧不同
        // → AtlasTextOp cache miss → 重新光栅化 ~600μs
    }
}

三者对比总结

┌────────────────────────────────────────────────────────────────────────────┐
│ TimeView (hour_view) — 时钟大数字 "10:30"                                   │
│                                                                            │
│ 类继承: View(非 TextView)                                                 │
│ 绘制方式: canvas.drawPath(mTextPath, paint)                                │
│ 渲染路径: Path → GPU tessellation → 顶点着色器直接变换                      │
│ AtlasTextOp: ❌ 完全不涉及                                                  │
│ Scale 影响: 无(Path 是矢量几何体,GPU 直接缩放顶点)                         │
│                                                                            │
│ 结果: 0 帧 AtlasTextOp, 0ms 开销                                           │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ ClassicTextAreaView (text_area) — 日期 "周一 6月2日"                         │
│                                                                            │
│ 类继承: TextView                                                            │
│ 绘制方式: canvas.drawText(text, x, y, paint)                               │
│ 渲染路径: drawText → drawTextBlob → AtlasTextOp                            │
│ AtlasTextOp: ✅ 走,但被 layer 保护                                         │
│                                                                            │
│ 保护机制:                                                                   │
│   父容器 KeyguardClockContainer:                                            │
│     hasOverlappingRendering = true (默认)                                   │
│     + transitionAlpha < 1                                                  │
│     → promotedToLayer() = true                                             │
│     → 形成 persistent hardware layer                                       │
│     → 内容只首帧绘制一次,缓存为 GPU 纹理                                    │
│     → 后续帧 scale/alpha 变化只做 GPU 纹理变换                               │
│                                                                            │
│ 结果: 仅首帧 1 次 AtlasTextOp (0.146ms), 后续帧 0ms                        │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ KeyguardIndicationTextView — "上滑解锁" / 充电状态                           │
│                                                                            │
│ 类继承: TextView                                                            │
│ 绘制方式: TextView.onDraw() → Layout.draw() → drawTextBlob                 │
│ 渲染路径: drawText → drawTextBlob → AtlasTextOp                            │
│ AtlasTextOp: ✅ 走,且每帧 cache miss                                       │
│                                                                            │
│ 无保护:                                                                     │
│   父容器 KeyguardBottomAreaView:                                            │
│     hasOverlappingRendering = false (MIUI override!)                       │
│     + transitionAlpha < 1                                                  │
│     → promotedToLayer() = false ← 条件不满足!                              │
│     → 不形成 layer                                                          │
│     → AlphaFilterCanvas 每帧逐元素重放 display list                          │
│     → canvas CTM 含父级 scale → 亚像素偏移每帧不同                           │
│     → SkPackedGlyphID key 每帧不同 → cache miss                            │
│                                                                            │
│ 结果: 55 帧持续 AtlasTextOp, 总计 20.9ms                                   │
└────────────────────────────────────────────────────────────────────────────┘

根本差异链路图

父级 scale + alpha 动画
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│ 判断: 父容器 hasOverlappingRendering ?                        │
├────────────────┬────────────────────────────────────────────┤
│ = true (默认)  │  = false (MIUI override)                    │
│                │                                             │
│ promotedToLayer│  promotedToLayer = false                    │
│ = true         │  → 不形成 layer                             │
│ → 形成 layer   │  → AlphaFilterCanvas 每帧重放               │
│                │                                             │
│ ┌────────────┐ │  ┌──────────────────────────────────────┐  │
│ │ 判断: View │ │  │ drawText → AtlasTextOp               │  │
│ │ 绘制方式?  │ │  │ + canvas CTM 含 scale                 │  │
│ ├────────────┤ │  │ → mappedPos 亚像素每帧不同             │  │
│ │drawPath    │ │  │ → SkPackedGlyphID key 变化             │  │
│ │(TimeView)  │ │  │ → CACHE MISS → 重新光栅化 ~600μs      │  │
│ │→ 不走atlas │ │  └──────────────────────────────────────┘  │
│ │→ 0ms       │ │                                             │
│ ├────────────┤ │  KeyguardIndicationTextView:                │
│ │drawText    │ │  55帧 × 0.6ms = 20.9ms                     │
│ │(ClassicTV) │ │                                             │
│ │→ 首帧走atlas│ │                                             │
│ │→ layer缓存 │ │                                             │
│ │→ 后续0ms   │ │                                             │
│ └────────────┘ │                                             │
│                │                                             │
│ Clock: 0.146ms │  BottomArea: 20.9ms                         │
│ (一次性)       │  (持续性)                                    │
└────────────────┴────────────────────────────────────────────┘

证据总结

#证据文件:行号证明了什么
1TimeView : View (非 TextView)allInOne/TimeView.kt:20不继承 TextView 的 drawText 路径
2canvas.drawPath(mTextPath, paint)allInOne/TimeView.kt:103走 Path 几何渲染,不走 glyph atlas
3ClassicTextAreaView extends TextViewclassic/ClassicTextAreaView.java:45继承 TextView
4canvas.drawText(text, x, y, paint)classic/ClassicTextAreaView.java:816走 drawText → AtlasTextOp
5KeyguardClockContainerhasOverlappingRendering override全文搜索确认默认 true → promotedToLayer 生效
6KeyguardBottomAreaView.hasOverlappingRendering() = falseKeyguardBottomAreaView.kt:47-49阻止 promotedToLayer
7promotedToLayer 条件: alpha<1 && hasOverlappingRenderProperties.h:649-655时钟满足,底部不满足
8prepareLayer 重置 damage: 属性变化不 dirty layer 内容RenderNode.cpp:294-296Layer 后续帧不重绘
9Layer composite: makeImageSnapshot + drawImageRectRenderNodeDrawable.cpp:425-464GPU 纹理变换,不重放 DL
10无 layer 时: AlphaFilterCanvas + displayList->drawRenderNodeDrawable.cpp:639-667每帧重放 DL
11Trace: ClassicTextAreaView 在 “drawLayer [FrameLayout]” 下perfetto trace 数据确认在 layer 内
12Trace: KeyguardIndicationTextView 在 “renderFrameImpl” 下perfetto trace 数据确认不在 layer 内

延伸:为什么 MIUI 要设 hasOverlappingRendering=false

KeyguardBottomAreaView.hasOverlappingRendering() = false 的原始意图可能是:

  1. 避免 promotedToLayer 的 GPU 内存开销:persistent layer 需要分配一块等大的 GPU 纹理(1672×2364 的 bottom area → ~15MB RGBA)
  2. 避免 layer 创建/销毁的开销:动画开始/结束时 layer 的分配和释放
  3. 认为底部区域内容简单:只有两行文字,不需要 layer 保护

但这个优化决策产生了意外的副作用——失去了 layer 的缓存保护后,AlphaFilterCanvas 每帧逐元素重放 display list,叠加父级 scale 导致的亚像素偏移,引发了 AtlasTextOp 的持续 cache miss。

正确的修复方向不是恢复 hasOverlappingRendering=true(GPU 内存开销大),而是使用 setForceSDFT(true) 在动画期间切换为 SDF 文字渲染(glyph key 不含亚像素位置,cache 始终命中)。