1. 问题描述
Android 17 中存在一个窗口层级相关的回归问题:默认文本 Toast 和持有 INTERNAL_SYSTEM_WINDOW 权限的系统应用发出的自定义 Toast 在锁屏后仍然可见,而 Android 16 及更早版本中,锁屏时所有 Toast 均会被 Keyguard 正确覆盖。
复现步骤:在任意应用中触发标准文本 Toast 后立即按电源键锁屏,可观察到 Toast 内容持续显示在锁屏界面上方。使用系统签名应用发出自定义 Toast 同样可复现。
2. 根因定位
2.1 相关代码位置
问题位于 frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.java。WindowManagerPolicy 是一个 Java interface,继承自 WindowManagerPolicyConstants。其中的 default 方法 getWindowLayerFromTypeLw 负责将窗口类型映射到 Z 轴层级,决定各窗口在屏幕上的显示顺序。
该方法的完整签名为:
其中 canAddInternalSystemWindow 参数反映窗口所属进程是否持有 android.Manifest.permission.INTERNAL_SYSTEM_WINDOW 权限。该参数由 WindowState.canAddInternalSystemWindow() 提供:
2.2 Android 16 及之前的实现
在 AOSP 16(android-16.0.0_r1)中,TYPE_TOAST 分支直接返回固定值 7,不使用 canAddInternalSystemWindow 参数:
TYPE_TOAST 返回的 layer 值为 7。而锁屏及通知栏对应的 TYPE_NOTIFICATION_SHADE 返回 17。由于 7 < 17,Toast 窗口在 Z 轴上位于锁屏窗口之下,锁屏时会被正确覆盖。
2.3 Android 17 的变更
在 AOSP 17(android-17.0.0_r1)中,TYPE_TOAST 分支被修改为使用 canAddInternalSystemWindow 参数进行条件判断。实际代码如下:
变更逻辑为:
- 若窗口所属进程持有
INTERNAL_SYSTEM_WINDOW权限 → 返回27 - 否则 → 保持原有的
7
从注释可以看出,该改动的设计意图是区分「旧式 Toast」(legacy toast,可能携带任意 View,存在触控劫持风险)和「系统托管的 Toast」(system-managed toast,安全可控)。旧式 Toast 保持低层级,系统托管的 Toast 获得更高可见性。
2.4 Layer 层级对比
变更前后的关键窗口层级如下:
| Layer | 窗口类型 | 说明 |
|---|---|---|
| 7 | TYPE_TOAST(无权限) | Toast(旧行为及新行为中无权限情形) |
| 17 | TYPE_NOTIFICATION_SHADE | 通知面板 / 锁屏 |
| 27 | TYPE_TOAST(有权限) | Toast(新行为中有权限情形) |
27 > 17,因此被分配 layer 27 的 Toast 窗口在 Z 序上高于锁屏窗口。
2.5 问题的直接原因
layer 27 的分配并非无条件对所有 Toast 生效,而是按窗口的实际创建进程是否满足 canAddInternalSystemWindow 条件来决定。在 Android 17 的架构下,以下两类 Toast 会命中 canAddInternalSystemWindow ? 27 : 7 的 true 分支:
2.5.1 默认文本 Toast:SystemUI 代理渲染(Android 12+)
Android 系统从 Android 12 开始强制应用使用系统的 Toast 渲染管道:当任意应用调用 Toast.makeText().show() 时,Toast 的实际渲染由 SystemUI 进程(com.android.systemui)执行。
因此在 WindowManagerPolicy.getWindowLayerFromTypeLw 中:
- Toast 窗口由 SystemUI 进程创建。
-
WindowState.canAddInternalSystemWindow()对 SystemUI 返回true(SystemUI 持有INTERNAL_SYSTEM_WINDOW权限)。 -
TYPE_TOAST分支命中canAddInternalSystemWindow ? 27 : 7的 true 分支。 - 返回 layer 27,高于
TYPE_NOTIFICATION_SHADE的 layer 17。
关键问题在于:权限检查的对象是 Toast 窗口的实际渲染进程(SystemUI),而非发起 Toast 请求的原始应用。无论第三方应用是否有系统权限,其文本 Toast 都会经由 SystemUI 取得 layer 27 的层级提升。
2.5.2 有 INTERNAL_SYSTEM_WINDOW 权限的应用的自定义 Toast
对于通过 setView() 设置自定义视图的 Toast,窗口仍由应用自身进程创建(不走 SystemUI 代理)。此时 canAddInternalSystemWindow 直接反映调用进程的权限:
- 持有
INTERNAL_SYSTEM_WINDOW权限的系统签名应用 / privileged 应用调用Toast.setView().show()。 -
WindowState.canAddInternalSystemWindow()对调用进程返回true。 -
TYPE_TOAST分支命中canAddInternalSystemWindow ? 27 : 7的 true 分支。 - 返回 layer 27,高于
TYPE_NOTIFICATION_SHADE的 layer 17。
而对于普通第三方应用发出的自定义 Toast,canAddInternalSystemWindow 返回 false,layer 保持 7,行为与 Android 16 一致——锁屏时仍被遮挡。
2.6 第二块拼图:DisplayPolicy.applyKeyguardPolicy 如何将 layer 变更兑现为可见性
2.3 节回答了「为什么 Toast 的 layer 变成了 27」,本节回答「layer 27 为什么导致锁屏可见」——后者是 DisplayPolicy.applyKeyguardPolicy() 的职责。
2.6.1 applyKeyguardPolicy —— 隐藏决策入口
DisplayPolicy.applyKeyguardPolicy() 在锁屏状态变化时遍历所有窗口,调用 canBeHiddenByKeyguard() 逐窗口判断是否应被 Keyguard 遮挡:
2.6.2 canBeHiddenByKeyguard —— 16 和 17 代码一致,未变更
本节关键结论:canBeHiddenByKeyguard() 在 Android 16 和 17 中的代码完全相同,并未发生任何修改。两版源码一致为:
TYPE_TOAST 不在 switch 的任何 case 分支中,落入 default。default 的语义为:
窗口的实际 layer 低于锁屏窗口(TYPE_NOTIFICATION_SHADE)的 layer 时,才被隐藏。
这是 Android 16 就已经存在的逻辑。
2.6.3 为什么 16 上没问题、17 上出问题了?
答案很简单:layer 变了。
| 版本 | Toast Layer | 比较 layer < 17 |
canBeHiddenByKeyguard 返回值 | 锁屏行为 | |
|---|---|---|---|---|---|
| Android 16 | 7(固定) | 7 < 17 |
= true
|
true |
Toast 被隐藏 ✅ |
| Android 17(无权限) | 7 | 7 < 17 |
= true
|
true |
Toast 被隐藏 ✅ |
| Android 17(SystemUI / 系统签名应用) | 27 | 27 < 17 |
= false
|
false |
Toast 不被隐藏 ❌ |
layer 从 7 升至 27 是唯一的变更点。canBeHiddenByKeyguard() 自身未变——它始终做 layer 比较。在 16 上比较结果是 true,在 17 上当 canAddInternalSystemWindow = true 时比较结果变为 false。
canAddInternalSystemWindow() 的实现只是成员变量 getter,在窗口创建时由进程权限决定:
SystemUI 进程持有 INTERNAL_SYSTEM_WINDOW 权限 → mOwnerCanAddInternalSystemWindow = true → getWindowLayerFromTypeLw 返回 27 → 27 < 17 = false → canBeHiddenByKeyguard 返回 false → Toast 穿透锁屏。
2.6.4 完整因果链
1. App 调用 Toast.makeText().show() ↓2. SystemUI 代理渲染 Toast 窗口 ↓3. WindowManagerPolicy.getWindowLayerFromTypeLw(TYPE_TOAST, canAddInternalSystemWindow=true) ★ 唯一变更点:AOSP 16 固定返回 7 → AOSP 17 改为 canAddInternalSystemWindow ? 27 : 7 ↓4. Toast 窗口获得 layer = 27(高于 TYPE_NOTIFICATION_SHADE 的 17) ↓5. DisplayPolicy.applyKeyguardPolicy() 遍历窗口 ↓6. WindowState.canBeHiddenByKeyguard() 判断 → default 分支:layer (27) < TYPE_NOTIFICATION_SHADE layer (17) ? → 27 < 17 = false → 返回 false(不隐藏) ↓7. Toast 穿透锁屏,锁屏后仍可见 ❌
整条链路的唯一变更点在第三步——getWindowLayerFromTypeLw 中 TYPE_TOAST 分支从固定返回 7 改为 canAddInternalSystemWindow ? 27 : 7。后续所有方法代码均未变动,只是输入(layer 值)不同导致输出不同。
3. 影响范围
该问题影响以下两类 Toast:
-
所有应用发出的默认文本 Toast(
Toast.makeText())—— 因为 Android 12+ 强制 SystemUI 代理所有文本 Toast 的渲染,SystemUI 持有INTERNAL_SYSTEM_WINDOW权限,所有文本 Toast 被分配 layer 27。 -
持有
INTERNAL_SYSTEM_WINDOW权限的应用发出的自定义 Toast —— 此类应用(系统签名应用 / privileged 应用)自身即满足canAddInternalSystemWindow条件,其自定义 Toast 同样被分配 layer 27。
不受影响:普通第三方应用通过 setView() 发出的自定义 Toast(layer 保持 7,仍被锁屏遮挡)。
当上述两类 Toast 显示期间用户锁屏时,Toast 内容将暴露在锁屏界面之上。
4. 修复建议
问题的根因是 getWindowLayerFromTypeLw 中 TYPE_TOAST 被分配了高于锁屏的 layer 27,而 canBeHiddenByKeyguard() 的 default 分支仅做 layer 比较,未对 Toast 做特殊处理,导致 layer 27 的 Toast 穿透锁屏。
最简洁的修复方案是在 canBeHiddenByKeyguard() 中为 TYPE_TOAST 新增一个 case 分支,直接返回 true——因为 Android 16 中 Toast 的实际行为就是始终被 Keyguard 隐藏。这样无需改动 layer 分配逻辑,也不影响 layer 27 在其他场景下的作用(如非锁屏时的 Z 序提升)。
修改位置:
为什么不在 getWindowLayerFromTypeLw 中回退 layer?
getWindowLayerFromTypeLw 中的 layer 分配影响 Z 序(非锁屏场景下决定谁覆盖谁),有其独立的设计意图——系统托管的 Toast 在正常使用场景下确实应该高于某些悬浮窗。在 canBeHiddenByKeyguard 中处理,语义上更精确:「Toast 在锁屏时应当被隐藏」是一个锁屏策略决策,而非层级分配问题。
5. 总结
这不是一个由多条独立链路并发变更导致的复杂回归。根因只有一个:
唯一变更点:WindowManagerPolicy.getWindowLayerFromTypeLw 中 TYPE_TOAST 分支
AOSP 17 将 TYPE_TOAST 分支从固定返回 7 改为 canAddInternalSystemWindow ? 27 : 7。这一变更的意图是合理的——区分旧式 Toast(legacy,存在触控劫持风险)和系统托管的 Toast(system-managed,安全可控),给后者更高层级。
为什么波及默认文本 Toast 和有 INTERNAL_SYSTEM_WINDOW 权限的应用?
Android 12+ 强制 SystemUI 代理所有文本 Toast 的渲染。此外,持有 INTERNAL_SYSTEM_WINDOW 权限的系统签名应用发出的自定义 Toast 也直接满足条件。两者的共同点是 canAddInternalSystemWindow 为 true,因此 getWindowLayerFromTypeLw 返回 27。
canAddInternalSystemWindow 的取值来源于窗口的实际创建进程(SystemUI)或持权应用本身,因此所有文本 Toast 和有权限的自定义 Toast 都被分配 layer 27。
为什么 layer 27 导致锁屏可见?
DisplayPolicy.applyKeyguardPolicy() 通过 canBeHiddenByKeyguard() 判断是否隐藏窗口。后者在 Android 16/17 中代码完全一致,其逻辑为:窗口的实际 layer < 锁屏层(17)时隐藏。
- layer 7 →
7 < 17=true→ 隐藏(Android 16 行为) - layer 27 →
27 < 17=false→ 不隐藏(Android 17 问题)
canBeHiddenByKeyguard 本身未变,变更的只是传入的 layer 值。
完整因果链
Toast.makeText().show()
→ SystemUI 代理渲染
→ WindowManagerPolicy.getWindowLayerFromTypeLw()
→ canAddInternalSystemWindow = true(SystemUI 持有权限)
→返回 layer 27(AOSP 17 新增逻辑;AOSP 16 固定返回 7)
→ DisplayPolicy.applyKeyguardPolicy()
→ canBeHiddenByKeyguard()
→ layer(27) < LOCK_SCREEN_LAYER(17) ?
→ false → 不隐藏 → Toast 穿透锁屏 ❌
在 AOSP 源码中可通过以下路径定位相关代码:
- 文件:
frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.java - 方法:
getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow, boolean roundedCornerOverlay)(变更点) - 文件:
frameworks/base/services/core/java/com/android/server/wm/WindowState.java
• canAddInternalSystemWindow() 方法
• canBeHiddenByKeyguard() 方法(16/17 代码未变)
- 文件:
frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java
• applyKeyguardPolicy() 方法
- 层级常量:
frameworks/base/core/java/android/view/WindowManagerPolicyConstants.java
本文基于 AOSP android-16.0.0_r1 与 android-17.0.0_r1 源码对比分析。








Top comments (0)