锁屏快捷方式上划离屏 drawLayer 性能问题分析

一、问题背景

1.1 现象描述

锁屏状态下,用户上划底部快捷方式打开应用时,Perfetto trace 中观察到离屏渲染:

drawLayer [FrameLayout] 1224.0 x 2912.0
alpha caused saveLayer 1224x2912

Layer 尺寸为 1224 x 2912(全屏大小),在快捷方式上划动画期间持续被 GPU 渲染,造成不必要的 GPU 开销和掉帧。

  • 问题帧: DrawFrames 20238
  • Trace 文件: perfetto_20260228143052.pftrace

1.2 涉及模块

模块路径仓库
锁屏面板布局packages/SystemUI/miui/Keyguard/res/layout/keyguard_panel_view.xmlMiuiSystemUI
锁屏面板控制器packages/SystemUI/miui/Keyguard/src/.../KeyguardPanelViewController.ktMiuiSystemUI
快捷方式控制器packages/SystemUI/miui/Keyguard/src/.../MiuiShortcutController.ktMiuiSystemUI
主窗口 Viewpackages/SystemUI/src/.../NotificationShadeWindowView.javaMiuiSystemUI
快捷方式插件keyguardshortcuts/src/.../ShortcutWindowManager.ktMiuiAod (/home/zbc/pangu/MiuiAod)

二、Perfetto Trace 时间线分析

2.1 DrawFrames 20238 完整时间线

以下为问题帧 DrawFrames 20238 (t=244.479s) 前后的关键事件:

244.452s  InputConsumer: miui_keyguard_shortcut 窗口创建
244.452s  ShortcutWindowManager$addRootViewToWindow — 插件创建独立窗口
244.454s  measure: ShortcutOccludedAnimView + ScaleShortcutImageView
244.455s  VRI[miui_keyguard_shortcut] relayoutWindow#first=true — 窗口首次 relayout
244.464s  measure/layout: ShortcutOccludedAnimView(第二次)
244.464s  draw-VRI[miui_keyguard_shortcut] — 快捷方式窗口首次绘制
244.470s  VRI[miui_keyguard_shortcut]-reportDrawFinished seqId=0
244.477s  DefineClass: com/miui/keyguard/shortcuts/utils/AnimationUtils(插件类加载)

=== DrawFrames 20238 开始 (244.479s) ===

244.479s  syncAndDrawFrame
244.479s  DrawFrames 20238
244.480s  dequeueBuffer - VRI[NotificationShade] ← 确认以下绘制属于主窗口
244.480s  Drawing 0.00 0.00 1224.00 2912.00 — 主窗口 NotificationShade 绘制
244.480s  renderFrame
244.480s  ⚠️ drawLayer [FrameLayout] 1224.0 x 2912.0 — 问题1: keyguard_info_layer 硬件 Layer
244.480s  draw-VRI[miui_keyguard_shortcut] — 快捷方式窗口绘制(主线程)
244.480s  draw:ShortcutOccludedAnimView
244.480s  onDraw:ShortcutOccludedAnimView
244.480s  draw:ScaleShortcutImageView
244.480s  onDraw:ScaleShortcutImageView

=== 下一帧渲染 ===

244.480s  syncAndDrawFrame
244.481s  FrameLayout → FrameLayout
244.481s  ⚠️ alpha caused saveLayer 1224x2912 — 问题2: 主窗口 alpha saveLayer
244.481s  AlphaOptimizedFrameLayout
244.481s  FrameLayout...

2.2 离屏渲染窗口归属确认

通过 trace 中的 dequeueBuffer - VRI[NotificationShade] 确认,两个离屏渲染均发生在主 SystemUI 窗口 (NotificationShade)

Trace 现象归属窗口根因
drawLayer [FrameLayout] 1224.0 x 2912.0NotificationShade (主窗口)keyguard_info_layerelevation="1px"
alpha caused saveLayer 1224x2912NotificationShade (主窗口)NotificationShadeWindowView.alpha 动画

: 快捷方式插件窗口 VRI[miui_keyguard_shortcut] 的 rootView 和 BaseShortcutAnimView 均已覆写 hasOverlappingRendering() = false,且未设置 setLayerType(LAYER_TYPE_HARDWARE),不存在离屏问题。


三、根因分析

问题1: drawLayer [FrameLayout] 1224.0 x 2912.0 — keyguard_info_layer 硬件 Layer

来源: 主窗口 NotificationShade 内 keyguard_panel_view.xml 中的 keyguard_info_layer

<!-- keyguard_panel_view.xml(修改前)-->
<FrameLayout
    android:id="@+id/keyguard_info_layer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:elevation="1px">   ← 离屏根因

原因: Android Framework 对设置了 elevation 的 View,会创建独立的 RenderNode Hardware Layer(即 drawLayer)。渲染流程为:

每帧渲染流程:
  1. 分配 1224×2912 的离屏 GPU buffer
  2. 将 keyguard_info_layer 及所有子 View 绘制到该 buffer(离屏渲染)
  3. 计算 1px elevation 对应的阴影(几乎不可见)
  4. 将该 buffer 合成到最终 Surface

冗余性: keyguard_info_layerKeyguardPanelView 的最后一个子 View,绘制顺序天然最高,elevation="1px" 对 z-order 没有实际作用。

问题2: alpha caused saveLayer 1224x2912 — NotificationShadeWindowView alpha 动画

来源: NotificationShadeWindowView 的 alpha 动画

触发代码:

// KeyguardPanelViewController.kt:4422-4424
private fun hideWindowViewByOccludedAnim() {
    Folme.useAt(notificationShadeWindowView).state()
        .to(alphaHideState, hideEase)  // alpha 从 1 渐变到 0
}
 
// alphaHideState 定义 (KeyguardPanelViewController.kt:342)
private val alphaHideState = AnimState(TAG_KEYGUARD_OCCLUDED_ANIM)
    .add(ViewProperty.ALPHA, 0f)

原因:

  • Folme 动画通过 View.setAlpha() 设置 notificationShadeWindowView 的 alpha
  • 当 alpha ∈ (0, 1) 时,HWUI 检查 hasOverlappingRendering()
  • NotificationShadeWindowView 继承自 WindowRootView → FrameLayout,未覆写 hasOverlappingRendering(),默认返回 true
  • HWUI 因此创建全窗口 1224×2912 的 saveLayer(离屏 FBO),将所有子 View 渲染到 FBO 后再应用 alpha 合成

HWUI saveLayer 渲染流程:

每帧渲染(alpha ∈ (0,1) 期间):
  1. 检测到 ViewGroup.alpha != 1.0 && hasOverlappingRendering() == true
  2. 调用 Canvas.saveLayerAlpha() 分配 1224×2912 离屏 FBO
  3. 将所有子 View(通知面板、锁屏元素、状态栏等)渲染到 FBO
  4. 调用 Canvas.restore() 将 FBO 以指定 alpha 合成到主 Surface

四、快捷方式上划动画完整流程

用户上划快捷方式图标
  │
  ▼
MiuiShortcutPlugin.onTouchEvent() [Plugin 层处理手势]
  │
  ▼
MiuiShortcutPlugin 回调 CALLBACK_onOccludedAnimationStart
  │
  ▼
MiuiShortcutController.onShortcutPluginCallbackWrap()
  ├── handleOccludedAnimationStart()
  │     ├── ScenarioRecognitionEventUtils.onEventStart(KEYGUARD_SHORTCUT_OPEN_JANK)
  │     ├── BoostHelper.boost(500L)
  │     ├── KeyguardPanelViewInjector.shortcutOccludedAnimForAppStart()
  │     │     └── KeyguardPanelViewController.shortcutOccludedAnimForAppStart()
  │     │           ├── collapseNotification()
  │     │           ├── collapseControlCenter()
  │     │           └── hideWindowViewByOccludedAnim()
  │     │                 └── Folme alpha 1→0 动画 ← ⚠️ 触发 saveLayer
  │     └── keyguardViewMediatorInjector.beginCujOcclude(true)
  │
  ▼
MiuiShortcutPlugin 回调 CALLBACK_onFullscreenAnimationStart
  │
  ▼
MiuiShortcutController.handleFullscreenAnimationStart()
  └── KeyguardPanelViewInjector.shortcutFullscreenAnimStart()
        └── KeyguardPanelViewController.shortcutFullscreenAnimStart()
              └── hideWindowViewByOccludedAnim()  ← 再次确保 window 隐藏
  │
  ▼
MiuiShortcutPlugin 回调 CALLBACK_onOccludedAnimationEnd
  │
  ▼
MiuiShortcutController.handleOccludedAnimationEnd()
  ├── ScenarioRecognitionEventUtils.onEventEnd(KEYGUARD_SHORTCUT_OPEN_JANK)
  ├── KeyguardPanelViewInjector.shortcutOccludedAnimForAppEnd()
  │     └── KeyguardPanelViewController.shortcutOccludedAnimForAppEnd()
  │           └── IKeyguardViewMediatorInjector.updateOccluded()
  └── InteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION)

返回锁屏流程(恢复路径):

应用退回锁屏
  │
  ▼
CALLBACK_onUnoccludedAnimationStart
  └── KeyguardPanelViewController.shortcutUnoccludedAnimForAppStart()
        ├── showWindowViewByUnoccludedAnim()  ← window alpha: 0 → 1
        ├── setViewAlphaAndVisibleToShow(keyguardInfoLayer)
        ├── setViewAlphaAndVisibleToShow(notificationContainer)
        └── ...

五、解决方案

方案 A: NotificationShadeWindowView 覆写 hasOverlappingRendering()(已实施)

文件: packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java

修改内容:

// MIUI ADD: Keyguard_Shortcut
// Avoid alpha caused saveLayer for full window size (1224x2912) during
// shortcut occluded/unoccluded animation which animates this view's alpha.
@Override
public boolean hasOverlappingRendering() {
    return false;
}
// END Keyguard_Shortcut

原理:

  • 返回 false 后,HWUI 在 alpha ∈ (0,1) 时跳过 saveLayer 分配
  • alpha 直接应用到每个子 View 的绘制指令,避免全窗口离屏 FBO
  • 这是 SystemUI 中已有的成熟模式(AlphaOptimizedFrameLayout 采用相同方案)

影响范围评估:

场景alpha 值影响
正常锁屏显示1.0无影响,alpha=1 时 hasOverlappingRendering 不生效
快捷方式 occluded 动画1→0 渐变窗口整体淡出,视觉差异不可感知
快捷方式 unoccluded 动画0→1 渐变窗口整体淡入,视觉差异不可感知
window alpha=0(完全隐藏)0.0无影响,完全不可见
resetWindowAnimationAlphasetTo(1.0)无影响,直接设置无动画

视觉差异说明: hasOverlappingRendering()=false 时,重叠区域的子 View 各自独立应用 alpha,在半透明状态下重叠区域可能比预期略透。但由于此 alpha 动画仅在快捷方式过渡期间(约200-300ms)使用,且用户注意力在应用启动动画上,视觉差异不可感知。

方案 B: 移除 keyguard_info_layer 的 elevation=“1px”(已实施)

文件: packages/SystemUI/miui/Keyguard/res/layout/keyguard_panel_view.xml

<!-- 修改前 -->
<FrameLayout
    android:id="@+id/keyguard_info_layer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:elevation="1px">
 
<!-- 修改后 -->
<FrameLayout
    android:id="@+id/keyguard_info_layer"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

原理: 移除 elevation 后不再为该 View 创建独立硬件 Layer。该 View 是 KeyguardPanelView 最后一个子 View,绘制顺序天然最高,无需 elevation 保证 z-order。

快捷方式插件窗口(无需修改)

经查证,快捷方式插件源码位于 MiuiAod 仓库:

/home/zbc/pangu/MiuiAod/keyguardshortcuts/src/main/java/com/miui/keyguard/shortcuts/
├── manager/ShortcutWindowManager.kt      ← 创建 miui_keyguard_shortcut 窗口
├── view/BaseShortcutAnimView.kt          ← 动画基类
├── view/ShortcutOccludedAnimView.kt      ← occluded 动画 View
├── view/ViewBlurHelper.kt                ← 模糊辅助(未使用)
└── utils/BlurHelper.kt                   ← SurfaceControl 模糊(调用已注释)

确认无需修改的原因:

  1. ShortcutWindowManager.rootView 已覆写 hasOverlappingRendering() = false(ShortcutWindowManager.kt:53)
  2. BaseShortcutAnimView 已覆写 hasOverlappingRendering() = false(BaseShortcutAnimView.kt:149)
  3. 无任何 setLayerType(LAYER_TYPE_HARDWARE) 调用
  4. ViewBlurHelper 未被任何代码引用;BlurHelper.applyBlur() 调用已注释
  5. trace 中的 drawLayer [FrameLayout]dequeueBuffer - VRI[NotificationShade] 确认属于主窗口,非插件窗口

六、修改记录

日期修改文件修改内容仓库状态
2026-02-28NotificationShadeWindowView.java覆写 hasOverlappingRendering() 返回 falseMiuiSystemUI已完成
2026-02-28keyguard_panel_view.xml移除 keyguard_info_layerelevation="1px"MiuiSystemUI已完成

七、验证方法

7.1 Perfetto 验证

# 抓取 trace 后转换为 systrace 格式
trace_to_text systrace perfetto_xxx.pftrace | grep -E "saveLayer|drawLayer.*FrameLayout"

预期结果:

  • 方案 A 修复后: alpha caused saveLayer 1224x2912 不再出现
  • 方案 B 修复后: drawLayer [FrameLayout] 1224.0 x 2912.0 不再出现

7.2 功能回归测试

  • 锁屏快捷方式上划打开应用动画流畅
  • 应用返回锁屏动画正常
  • 锁屏画报信息正常显示
  • 人脸解锁图标正常显示
  • 通知栏/控制中心下拉正常
  • AOD 状态下锁屏元素正常
  • 锁屏编辑模式进出正常
  • 负一屏滑动正常

7.3 GPU 渲染验证

开启 开发者选项 → GPU 呈现模式分析,对比修改前后锁屏快捷方式上划时的帧渲染时间,预期绿色条(Draw)高度显著降低。