锁屏快捷方式上划离屏 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.xml | MiuiSystemUI |
| 锁屏面板控制器 | packages/SystemUI/miui/Keyguard/src/.../KeyguardPanelViewController.kt | MiuiSystemUI |
| 快捷方式控制器 | packages/SystemUI/miui/Keyguard/src/.../MiuiShortcutController.kt | MiuiSystemUI |
| 主窗口 View | packages/SystemUI/src/.../NotificationShadeWindowView.java | MiuiSystemUI |
| 快捷方式插件 | keyguardshortcuts/src/.../ShortcutWindowManager.kt | MiuiAod (/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.0 | NotificationShade (主窗口) | keyguard_info_layer 的 elevation="1px" |
alpha caused saveLayer 1224x2912 | NotificationShade (主窗口) | 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_layer 是 KeyguardPanelView 的最后一个子 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 | 无影响,完全不可见 |
| resetWindowAnimationAlpha | setTo(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 模糊(调用已注释)
确认无需修改的原因:
ShortcutWindowManager.rootView已覆写hasOverlappingRendering() = false(ShortcutWindowManager.kt:53)BaseShortcutAnimView已覆写hasOverlappingRendering() = false(BaseShortcutAnimView.kt:149)- 无任何
setLayerType(LAYER_TYPE_HARDWARE)调用 ViewBlurHelper未被任何代码引用;BlurHelper.applyBlur()调用已注释- trace 中的
drawLayer [FrameLayout]经dequeueBuffer - VRI[NotificationShade]确认属于主窗口,非插件窗口
六、修改记录
| 日期 | 修改文件 | 修改内容 | 仓库 | 状态 |
|---|---|---|---|---|
| 2026-02-28 | NotificationShadeWindowView.java | 覆写 hasOverlappingRendering() 返回 false | MiuiSystemUI | 已完成 |
| 2026-02-28 | keyguard_panel_view.xml | 移除 keyguard_info_layer 的 elevation="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)高度显著降低。