上滑进密码页离屏丢帧
背景
https://jira-phone.mioffice.cn/browse/BUGHQ-16261
上滑首帧,RenderThread 耗时造成严重丢帧。

分析
1. RenderThread 耗时主因:4 次 allocateImageMemory
锁屏上滑进密码页的首帧(DrawFrames),RenderThread 的 flush layers 阶段触发了多次 VulkanAMDMemoryAllocator::allocateImageMemory,每次耗时 ~9ms,总计 ~37ms,远超 8.33ms 帧周期(120Hz),直接导致丢帧。
本地(未加压)抓取 simpleperf ,单次 allocateImageMemory ~7ms 的耗时构成:
| 函数 | 占比 | 说明 |
|---|---|---|
__pi_clear_page | 63.80% | ARM64 页面清零(DC ZVA 指令,安全要求) |
_raw_spin_unlock_irqrestore | 12.53% | 内核自旋锁 |
folio_unlock | 6.66% | 页面管理 |
check_new_pages | 5.61% | 新页面检查 |
_kgsl_alloc_pages | 2.93% | KGSL GPU 驱动分配 |
kgsl_iopgtbl_map | 3.01% | GPU IOMMU 页表映射 |
每次分配 14,508,032 bytes(14.5MB,1280×2816 对齐后的 VkImage),需要 ~3,540 个 4KB 物理页面全部清零。
2. allocateImageMemory 的触发原因:alpha 导致的离屏渲染
2.1 触发链路
上滑手势 ACTION_MOVE
→ updateKeyguardElementsExpansionInternal(fraction)
→ doScaleAndAlpha(view, scale, infoAlpha)
→ view.transitionAlpha = infoAlpha (< 1.0) ← View 的 alpha 被设为 < 1
→ RenderNode.setAlpha(getFinalAlpha()) ← 更新 RenderNode 属性
↓
prepareTree 阶段:
→ effectiveLayerType() → promotedToLayer() 返回 true
→ 加入 LayerUpdateQueue
↓
renderFrame 阶段:
→ drawLayer [ViewName] WxH ← 渲染硬件图层内容
→ flush layers
→ GrResourceAllocator::assign()
→ Register::instantiateSurface()
→ createSurface() → allocateImageMemory() ← 分配 VkImage 显存2.2 为什么 alpha < 1 会产生离屏渲染
当一个 View 同时满足以下条件时,HWUI 将其自动提升(promotedToLayer)为硬件图层(离屏渲染):
// RenderProperties.h:643-649
if (!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 &&
mPrimitiveFields.mHasOverlappingRendering) {
return true; // → effectiveLayerType() = LayerType::RenderLayer
}离屏的目的: 如果一个 View 的子内容存在重叠绘制(hasOverlappingRendering = true),直接对每个 draw call 乘以 alpha 会导致重叠区域 alpha 被多次叠加(过度透明)。正确做法是先将所有子内容绘制到一个离屏缓冲区(alpha=1.0),再将整个缓冲区以目标 alpha 合成到屏幕上——这就是离屏渲染(hardware layer)。


每个被提升的 View 需要一个全屏尺寸(1280×2816)的 VkImage 作为离屏渲染目标。当该 VkImage 不在 Skia 的 GPU 资源缓存中时,触发 allocateImageMemory 新建。
hasOverlappingRendering() 本质上就是告诉系统:“我这个 View 内部的绘制结果,有没有同一像素被多条 draw call 覆盖的情况”。如果有,就必须用离屏缓冲区保证正确;如果没有(每个像素只被画一次),两种方式结果相同,可以安全跳过缓冲区。
2.3 trace 中的证据
kDynamicVKAllocateTrace 开关打开后,所有 promotedToLayer 日志均为同一原因:
promotedToLayer: mPrimitiveFields.mAlpha = 0.989318 (第 1 帧)
promotedToLayer: mPrimitiveFields.mAlpha = 0.977664 (第 2 帧)
promotedToLayer: mPrimitiveFields.mAlpha = 0.927066 (第 3 帧)
...递减至 ~0.001无 hasSelfBlurBlend、mImageFilter、mNeedLayerForFunctors 等其他触发条件。
3. 参与 drawLayer 的 5 个 View 及其 ID
| 序号 | trace 中的 drawLayer | View ID (app:id) | 定位依据 |
|---|---|---|---|
| 1 | drawLayer [FrameLayout] 1280.0 x 2772.0 | keyguard_signature_layer | prepareTree 中位于 KeyguardClockContainer 之前,无可见子 View |
| 2 | drawLayer [KeyguardClockContainer] 1280.0 x 2772.0 | miui_keyguard_clock_container | 类名唯一,含 ClassicMaxHourClockView |
| 3 | drawLayer [FrameLayout] 1280.0 x 2772.0 | miui_keyguard_foreground_clock_container | prepareTree 中含 ClassicMaxMinuteClockView 子节点 |
| 4 | drawLayer [FrameLayout] 1280.0 x 2772.0 | keyguard_info_layer | prepareTree 中含 LockScreenMagazinePreView 子节点 |
| 5 | drawLayer [SharedNotificationContainer] 1280.0 x 2772.0 | SharedNotificationContainer | 类名唯一(仅首次手势出现) |
定位方法:
kDynamicVKAllocateTrace开启后prepareTreeImpl输出完整 RenderNode 树(含 depth 层级和 View 类名)- 通过子 View 特征(ClassicMaxHourClockView、ClassicMaxMinuteClockView、LockScreenMagazinePreView)唯一确认父 FrameLayout
- 结合 Android Studio Layout Inspector 布局捕获的资源 ID 交叉验证
源码中的对应关系(KeyguardPanelViewController.kt:4149-4181):
| 源码变量 | View ID | 设置 alpha 行号 |
|---|---|---|
keyguardSignatureLayer | keyguard_signature_layer | line 4172 |
keyguardClockInjector.getView() | miui_keyguard_clock_container | line 4180 |
keyguardClockInjector.getSecondaryClockLayerView() | miui_keyguard_foreground_clock_container | line 4181 |
keyguardInfoLayer | keyguard_info_layer | line 4149 |
notificationContainerParent | SharedNotificationContainer | line 4161 |
3.1 所有 5 个 View 的 alpha 值完全一致
源码中 5 个 View 的 alpha 均来自同一变量 infoAlpha(KeyguardPanelViewController.kt:4139):
private fun updateKeyguardElementsExpansionInternal(fraction: Float) {
val sinOutAlpha = Math.max(0f, Math.min(1.0, 1 - Math.pow((1 - fraction).toDouble(), 2.0)).toFloat())
val infoAlpha = (fraction + sinOutAlpha) / 2 // ← 唯一 alpha 来源
val linkageStateAlpha = if (linkageOffState) 0f else infoAlpha // 正常 = infoAlpha
var keyguardInfoLayerAlpha = linkageStateAlpha // 正常 = infoAlpha
// 5 个 drawLayer View 统一设置同一 alpha:
doScaleAndAlpha(keyguardInfoLayer, ..., keyguardInfoLayerAlpha) // line 4149 → infoAlpha
doScaleAndAlpha(notificationContainerParent, ..., keyguardInfoLayerAlpha) // line 4161 → infoAlpha
doScaleAndAlpha(keyguardSignatureLayer, scale, linkageStateAlpha) // line 4172 → infoAlpha
doScaleAndAlpha(keyguardClockInjector.getView(), ..., infoAlpha) // line 4180 → infoAlpha
doScaleAndAlpha(keyguardClockInjector.getSecondaryClockLayerView(), ..., infoAlpha)// line 4181 → infoAlpha
}trace 验证:每帧 syncFrameState 期间 15 条 promotedToLayer 日志(5 View × 3 次调用)的 alpha 值完全一致。
3.2 3 个 View 完全不可见
3 个不可见 View 各分配 14.5MB VkImage 仅为渲染一张透明图。
3.2.1 trace 证据:drawLayer 内部仅有 Clear 操作
drawLayer [FrameLayout] #1 (keyguard_signature_layer, dur=0.048ms):
└── OpsTask::recordOp name:Clear ← 仅清除为透明,无任何绘制
drawLayer [FrameLayout] #3 (keyguard_info_layer, dur=0.042ms):
└── OpsTask::recordOp name:Clear ← 仅清除为透明,无任何绘制
drawLayer [SharedNotificationContainer] (dur=0.050ms):
└── OpsTask::recordOp name:Clear ← 仅清除为透明,无任何绘制对比有内容的 View:
drawLayer [KeyguardClockContainer] (dur=0.155ms):
├── OpsTask::recordOp name:Clear
└── MiuiTextGlassView → OpsTask::recordOp name:FillRectOp ← 有实际绘制
drawLayer [FrameLayout] #2 (miui_keyguard_foreground_clock_container, dur=0.202ms):
├── OpsTask::recordOp name:Clear
├── ClassicTextAreaView → OpsTask::recordOp name:AtlasTextOp ← 有文字绘制
└── MiuiTextGlassView → OpsTask::recordOp name:FillRectOp + DrawAtlasPathOp3.2.2 布局文件证据:子 View alpha=0.0 或内容为空
| View | 子 View 状态 | 说明 |
|---|---|---|
keyguard_signature_layer | signature_text_container: BKG:empty, a:0.0 | 无签名内容 |
signature_text: BKG:empty, a:0.0 | ||
keyguard_info_layer | LockScreenMagazinePreView: alpha=0.0 | 无杂志/壁纸描述 |
magazine_info_container: BKG:empty, a:0.0 | ||
SharedNotificationContainer | ShadeBackgroundView: BKG a:0.0 | 背景全透明 |
NotificationStackScrollLayout: 无 ExpandableNotificationRow | 无通知(场景为”锁屏无通知”) |
4. ACTION_DOWN 后首帧出帧原因
ACTION_DOWN 后、allocateImageMemory 首帧之前,有一帧额外出帧。
根因:KeyguardIndicationController.clearIndicationViewAnimation() 在 ACTION_DOWN 时清除锁屏指示文案 View 动画。
调用链(源码 + simpleperf 双重验证):
ACTION_DOWN (在 doFrame 外直接分发)
→ NotificationPanelViewController$TouchHandler.onTouchEvent
→ KeyguardPanelViewInjector.onTouchEvent (line 892)
→ keyguardIndicationInjector.onTouchEvent
→ KeyguardIndicationController.onTouchEvent (line 1891-1892)
→ if (ACTION_DOWN) clearIndicationViewAnimation()
→ mLockScreenIndicationView.clearAnimation() (line 1878)
→ View.invalidateParentIfNeeded()
→ parent.invalidate(true) → scheduleTraversals → scheduleVsyncLocked
→ updateDeviceEntryIndication(false) (line 1879)源码(KeyguardIndicationController.java:1876-1892):
public void clearIndicationViewAnimation() {
if (mLockScreenIndicationView != null) {
mLockScreenIndicationView.clearAnimation(); // 触发 parent invalidate
updateDeviceEntryIndication(false); // 更新指示文案
}
}
public void onTouchEvent(MotionEvent event, ...) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
clearIndicationViewAnimation(); // ACTION_DOWN 无条件调用
}
}被 invalidate 的 View: mLockScreenIndicationView(KeyguardIndicationTextView)的 parent LinearLayout,位于 KeyguardBottomAreaView 内。View.clearAnimation() 内部的 invalidateParentIfNeeded() 对 parent 调用 invalidate(true),即使无正在运行的动画也会触发。


优化方案对比
| 方案 1:只对可见的 View 设置 alpha | 方案 2:alpha 上移父 View | 方案 3:离屏View 设置 hasOverlappingRendering=false | 方案 4:去除 ACTION_DOWN 首帧 | 方案 5:上滑动画添加 tag | |
|---|---|---|---|---|---|
| 优化思路 | 判断 View 可见性,对 3 个不可见的 View 不设置 alpha。可见性:背景不为空 || (存在 visible 的子 view && 子 view alpha > 0) | 对离屏View 的公共父 View 统一设置 alpha,并令父 View hasOverlappingRendering=false | 对 5 个 View 设置 setHasOverlappingRendering(false) | clearIndicationViewAnimation() 中增加条件判断,避免无意义的 clearAnimation 触发 invalidate | 标记上滑动画区间,排除脚本误判 |
| 显存申请数量 | 5 → 2 | 5 → 0 | 5 → 0 | 不变 | 不变 |
| 预估耗时减少 | ~24ms | ~40ms | ~40ms | 无直接耗时减少 | 无直接耗时减少 |
| 视觉正确性 | ✅ 完全一致 | ✅ 非重叠部分一致 ❌ 重叠绘制不正确 | 与方案 2 一致 | ✅ 完全一致 | ✅ 完全一致 |
| 局限性 | 需要对设置 alpha 的 View 逐一识别可见性 | 1. 公共父 View 可能含有其它子 View 2. 内部重叠效果问题 | 存在内部重叠绘制时需 clipPath 消除重叠 | 1. 有动画场景仍会误判 2. 显存未下降 3. 首帧时延未降低 | 1. 显存未下降 2. 首帧时延未降低 |
| 收益 | 1. 减少 43.5 MB 内存申请 2. 首帧时延降低 ~24ms | 1. 减少 72.5 MB 内存申请 2. 首帧时延降低 ~40ms | 1. 减少 72.5 MB 内存申请 2. 首帧时延降低 ~40ms | 无动画场景不再误判丢帧 | 测试脚本不再误判丢帧 |