5. Window Type 深度解析:FIRST_APPLICATION_WINDOW 到 LAST_SYSTEM_WINDOW 的层级划分
各位同学,今天我们来啃一块硬骨头——Window Type。说白了,就是窗口的类型和层级。我刚开始接触这部分时,也觉得不就是个数字嘛,有啥好讲的?直到我在项目中遇到一个诡异的Bug:一个弹窗死活盖不住状态栏,折腾了两天才发现是Type选错了。嗯,从那以后,我再也不敢小看这几个数字了。
5.1 窗口类型的三大阵营
Android把窗口分成了三大类,每一类都有自己的地盘。你想想看,这就像一栋楼,不同楼层住着不同的人,各有各的规矩。
| 类型范围 | 常量定义 | 层级区间 | 典型用途 |
|---|---|---|---|
| 应用窗口 | FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW | 1 ~ 99 | Activity、Dialog |
| 子窗口 | FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW | 1000 ~ 1999 | PopupWindow、菜单 |
| 系统窗口 | FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW | 2000 ~ 2999 | 状态栏、Toast、输入法 |
我个人习惯把这三个区间记成「1-99是普通住户,1000-1999是附属设施,2000-2999是物业管理员」。这样一想,层级关系就清晰多了。
5.2 应用窗口:你的Activity住几楼?
应用窗口的Type范围是1到99。每个Activity默认对应一个应用窗口,Type是TYPE_APPLICATION,也就是1。但这里有个细节——Dialog也是应用窗口,Type也是1。
核心要点:应用窗口必须依附于一个Activity,不能独立存在。如果你尝试在Service里直接创建一个应用窗口,系统会直接给你抛异常。
我在项目中遇到过一个问题:客户要求做一个全局悬浮球,我一开始用了TYPE_APPLICATION,结果一按Home键就消失了。后来才明白,应用窗口的生命周期是跟着Activity走的,Activity不可见,窗口也就没了。
5.3 子窗口:为什么是1000起步?
子窗口的Type从1000开始,到1999结束。这类窗口必须有一个父窗口,不能独立存在。常见的子窗口有:
TYPE_APPLICATION_PANEL(1000) — 面板窗口,比如菜单TYPE_APPLICATION_MEDIA(1001) — 媒体窗口,比如视频浮窗TYPE_APPLICATION_SUB_PANEL(1002) — 子面板
你可能会问:为什么子窗口的起始值不是100,而是1000?
嗯,这里有个历史原因。早期Android设计时,给应用窗口留了99个位置,子窗口留了1000个位置。说白了,就是预留了足够的扩展空间。实际上,我们常用的子窗口类型也就那么几个,大部分都没用到。
避坑指南:我曾经在PopupWindow里嵌套另一个PopupWindow,结果第二个PopupWindow死活显示不出来。查了半天才发现,子窗口的层级不能超过父窗口的层级范围。简单说,子窗口的Z-order永远在父窗口之上,但不会超过父窗口所在层级的边界。
5.4 系统窗口:2000到2999的「特权阶层」
系统窗口是最高层级的窗口,Type从2000到2999。这类窗口不需要依附于任何Activity,可以在任何地方显示。常见的系统窗口有:
| Type值 | 常量名 | 用途 | 是否需要权限 |
|---|---|---|---|
| 2000 | TYPE_STATUS_BAR | 状态栏 | 系统级 |
| 2002 | TYPE_KEYGUARD | 锁屏 | 系统级 |
| 2003 | TYPE_SYSTEM_ALERT | 系统警告弹窗 | SYSTEM_ALERT_WINDOW |
| 2005 | TYPE_TOAST | Toast提示 | 无需 |
| 2010 | TYPE_INPUT_METHOD | 输入法 | 系统级 |
| 2038 | TYPE_APPLICATION_OVERLAY | 应用悬浮窗(Android 8.0+) | SYSTEM_ALERT_WINDOW |
这里我要特别强调一下TYPE_APPLICATION_OVERLAY。Android 8.0之后,Google把TYPE_SYSTEM_ALERT给废弃了,推荐使用TYPE_APPLICATION_OVERLAY。为什么?因为TYPE_SYSTEM_ALERT的层级太高,容易遮挡系统关键UI,比如状态栏、锁屏界面。而TYPE_APPLICATION_OVERLAY的层级在系统窗口里是最低的,刚好在状态栏下面。
注意:从Android 10开始,TYPE_APPLICATION_OVERLAY也需要SYSTEM_ALERT_WINDOW权限。而且,系统对悬浮窗的显示位置做了限制——不能显示在状态栏区域,也不能显示在导航栏区域。我曾经有个客户反馈悬浮球点不到,排查后发现是悬浮窗被系统裁剪了。
5.5 层级背后的Z-order排序逻辑
窗口的Type值决定了它的Z-order。值越大,层级越高。但这里有个细节:同一Type的窗口,后添加的会覆盖先添加的。说白了,就是先来后到。
我们来看一段核心源码(简化版):
// WindowManagerService.java
private int assignWindowLayer(WindowState w) {
int type = w.mAttrs.type;
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
return type + 1; // 应用窗口,1~99
} else if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
return type + 1000; // 子窗口,1000~1999
} else if (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW) {
return type + 2000; // 系统窗口,2000~2999
}
return type;
}
你看,系统给每个窗口分配了一个「层值」。应用窗口的层值在1~99之间,子窗口在1000~1999之间,系统窗口在2000~2999之间。这三个区间是严格隔离的——子窗口永远盖不住系统窗口,应用窗口也永远盖不住子窗口。
关键结论:窗口的层级由Type决定,Type值越大,层级越高。不同Type区间之间是「断层」的,不存在跨区覆盖的可能。
5.6 一张图看懂窗口层级体系
下面我用一张SVG图来展示整个窗口层级体系,这样你就能直观地看到各个窗口的「楼层」关系了。
从这张图可以清楚地看到,三个层级区间是严格隔离的。系统窗口在最顶层,子窗口在中间,应用窗口在最底层。每个区间内部,Type值越大,层级越高。
5.7 实战:如何选择合适的Window Type?
说了这么多理论,我们来点实际的。你在开发中该怎么选Type?我总结了一个简单的决策流程:
- 你的窗口需要跟随Activity吗? 是 → 用应用窗口(Type=1)
- 你的窗口是某个View的附属吗? 是 → 用子窗口(Type=1000~1002)
- 你的窗口需要全局显示吗? 是 → 用系统窗口
- 系统窗口里,需要显示在状态栏之上吗? 是 → 用TYPE_SYSTEM_ALERT(已废弃)或系统级Type
- 只是普通悬浮窗? 用TYPE_APPLICATION_OVERLAY(Android 8.0+推荐)
个人经验:我建议普通App开发者尽量使用TYPE_APPLICATION_OVERLAY。虽然它需要权限,但兼容性最好。我曾经用TYPE_SYSTEM_ALERT在小米手机上踩过坑,MIUI对系统窗口做了特殊处理,导致悬浮窗显示异常。换成TYPE_APPLICATION_OVERLAY后问题就解决了。
5.8 避坑指南:Type选择不当的后果
我整理了几个常见的坑,都是我在项目中真实遇到过的:
- 坑1:用TYPE_SYSTEM_ALERT做悬浮窗 — Android 8.0之后会被系统限制,部分厂商ROM直接禁止显示。解决方案:改用TYPE_APPLICATION_OVERLAY。
- 坑2:在Service里创建TYPE_APPLICATION窗口 — 系统会抛
BadTokenException。解决方案:使用系统窗口Type。 - 坑3:子窗口的Type值超过1999 — 系统会将其视为系统窗口,但行为不可预期。解决方案:严格控制在1000~1999之间。
- 坑4:多个悬浮窗Type相同导致覆盖顺序混乱 — 同一Type的窗口按添加顺序排列,后添加的在上。解决方案:如果需要控制顺序,可以动态调整WindowManager.LayoutParams的token。
特别提醒:从Android 14开始,系统对TYPE_APPLICATION_OVERLAY的权限要求更加严格。应用必须在前台才能显示悬浮窗,否则会被系统自动隐藏。我曾经有个后台音乐播放器的悬浮窗需求,最后不得不改用前台Service+通知栏的方案。
好了,关于Window Type的层级划分就讲到这里。记住三个区间、记住Type值越大层级越高、记住系统窗口是特权阶层。下次遇到窗口显示异常的问题,先检查Type选对了没有——八成问题就出在这里。