上滑进密码页离屏丢帧

背景

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_page63.80%ARM64 页面清零(DC ZVA 指令,安全要求)
_raw_spin_unlock_irqrestore12.53%内核自旋锁
folio_unlock6.66%页面管理
check_new_pages5.61%新页面检查
_kgsl_alloc_pages2.93%KGSL GPU 驱动分配
kgsl_iopgtbl_map3.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 覆盖的情况”。如果有,就必须用离屏缓冲区保证正确;如果没有(每个像素只被画一次),两种方式结果相同,可以安全跳过缓冲区。

参考文档:OverlappingRendering 效果演示报告

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

hasSelfBlurBlendmImageFiltermNeedLayerForFunctors 等其他触发条件。

3. 参与 drawLayer 的 5 个 View 及其 ID

序号trace 中的 drawLayerView ID (app:id)定位依据
1drawLayer [FrameLayout] 1280.0 x 2772.0keyguard_signature_layerprepareTree 中位于 KeyguardClockContainer 之前,无可见子 View
2drawLayer [KeyguardClockContainer] 1280.0 x 2772.0miui_keyguard_clock_container类名唯一,含 ClassicMaxHourClockView
3drawLayer [FrameLayout] 1280.0 x 2772.0miui_keyguard_foreground_clock_containerprepareTree 中含 ClassicMaxMinuteClockView 子节点
4drawLayer [FrameLayout] 1280.0 x 2772.0keyguard_info_layerprepareTree 中含 LockScreenMagazinePreView 子节点
5drawLayer [SharedNotificationContainer] 1280.0 x 2772.0SharedNotificationContainer类名唯一(仅首次手势出现)

定位方法:

  • kDynamicVKAllocateTrace 开启后 prepareTreeImpl 输出完整 RenderNode 树(含 depth 层级和 View 类名)
  • 通过子 View 特征(ClassicMaxHourClockView、ClassicMaxMinuteClockView、LockScreenMagazinePreView)唯一确认父 FrameLayout
  • 结合 Android Studio Layout Inspector 布局捕获的资源 ID 交叉验证

源码中的对应关系(KeyguardPanelViewController.kt:4149-4181):

源码变量View ID设置 alpha 行号
keyguardSignatureLayerkeyguard_signature_layerline 4172
keyguardClockInjector.getView()miui_keyguard_clock_containerline 4180
keyguardClockInjector.getSecondaryClockLayerView()miui_keyguard_foreground_clock_containerline 4181
keyguardInfoLayerkeyguard_info_layerline 4149
notificationContainerParentSharedNotificationContainerline 4161

3.1 所有 5 个 View 的 alpha 值完全一致

源码中 5 个 View 的 alpha 均来自同一变量 infoAlphaKeyguardPanelViewController.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 + DrawAtlasPathOp

3.2.2 布局文件证据:子 View alpha=0.0 或内容为空

View子 View 状态说明
keyguard_signature_layersignature_text_container: BKG:empty, a:0.0无签名内容
signature_text: BKG:empty, a:0.0
keyguard_info_layerLockScreenMagazinePreView: alpha=0.0无杂志/壁纸描述
magazine_info_container: BKG:empty, a:0.0
SharedNotificationContainerShadeBackgroundView: 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 → 25 → 05 → 0不变不变
预估耗时减少~24ms~40ms~40ms无直接耗时减少无直接耗时减少
视觉正确性✅ 完全一致✅ 非重叠部分一致 ❌ 重叠绘制不正确与方案 2 一致✅ 完全一致✅ 完全一致
局限性需要对设置 alpha 的 View 逐一识别可见性1. 公共父 View 可能含有其它子 View 2. 内部重叠效果问题存在内部重叠绘制时需 clipPath 消除重叠1. 有动画场景仍会误判 2. 显存未下降 3. 首帧时延未降低1. 显存未下降 2. 首帧时延未降低
收益1. 减少 43.5 MB 内存申请 2. 首帧时延降低 ~24ms1. 减少 72.5 MB 内存申请 2. 首帧时延降低 ~40ms1. 减少 72.5 MB 内存申请 2. 首帧时延降低 ~40ms无动画场景不再误判丢帧测试脚本不再误判丢帧