为什么 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,但其父容器KeyguardClockContainer的hasOverlappingRendering=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 变化时:
- display list 中录制的是 Path 的几何数据(控制点坐标)
- 回放时 canvas CTM 含父级 scale
- GPU 顶点着色器直接对控制点做矩阵变换
- 光栅化在变换后的坐标空间进行 — 不需要 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 │
│ (一次性) │ (持续性) │
└────────────────┴────────────────────────────────────────────┘
证据总结
| # | 证据 | 文件:行号 | 证明了什么 |
|---|---|---|---|
| 1 | TimeView : View (非 TextView) | allInOne/TimeView.kt:20 | 不继承 TextView 的 drawText 路径 |
| 2 | canvas.drawPath(mTextPath, paint) | allInOne/TimeView.kt:103 | 走 Path 几何渲染,不走 glyph atlas |
| 3 | ClassicTextAreaView extends TextView | classic/ClassicTextAreaView.java:45 | 继承 TextView |
| 4 | canvas.drawText(text, x, y, paint) | classic/ClassicTextAreaView.java:816 | 走 drawText → AtlasTextOp |
| 5 | KeyguardClockContainer 无 hasOverlappingRendering override | 全文搜索确认 | 默认 true → promotedToLayer 生效 |
| 6 | KeyguardBottomAreaView.hasOverlappingRendering() = false | KeyguardBottomAreaView.kt:47-49 | 阻止 promotedToLayer |
| 7 | promotedToLayer 条件: alpha<1 && hasOverlapping | RenderProperties.h:649-655 | 时钟满足,底部不满足 |
| 8 | prepareLayer 重置 damage: 属性变化不 dirty layer 内容 | RenderNode.cpp:294-296 | Layer 后续帧不重绘 |
| 9 | Layer composite: makeImageSnapshot + drawImageRect | RenderNodeDrawable.cpp:425-464 | GPU 纹理变换,不重放 DL |
| 10 | 无 layer 时: AlphaFilterCanvas + displayList->draw | RenderNodeDrawable.cpp:639-667 | 每帧重放 DL |
| 11 | Trace: ClassicTextAreaView 在 “drawLayer [FrameLayout]” 下 | perfetto trace 数据 | 确认在 layer 内 |
| 12 | Trace: KeyguardIndicationTextView 在 “renderFrameImpl” 下 | perfetto trace 数据 | 确认不在 layer 内 |
延伸:为什么 MIUI 要设 hasOverlappingRendering=false
KeyguardBottomAreaView.hasOverlappingRendering() = false 的原始意图可能是:
- 避免 promotedToLayer 的 GPU 内存开销:persistent layer 需要分配一块等大的 GPU 纹理(1672×2364 的 bottom area → ~15MB RGBA)
- 避免 layer 创建/销毁的开销:动画开始/结束时 layer 的分配和释放
- 认为底部区域内容简单:只有两行文字,不需要 layer 保护
但这个优化决策产生了意外的副作用——失去了 layer 的缓存保护后,AlphaFilterCanvas 每帧逐元素重放 display list,叠加父级 scale 导致的亚像素偏移,引发了 AtlasTextOp 的持续 cache miss。
正确的修复方向不是恢复 hasOverlappingRendering=true(GPU 内存开销大),而是使用 setForceSDFT(true) 在动画期间切换为 SDF 文字渲染(glyph key 不含亚像素位置,cache 始终命中)。