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图来展示整个窗口层级体系,这样你就能直观地看到各个窗口的「楼层」关系了。

Android 窗口层级体系(Z-order) 系统窗口(2000 ~ 2999) TYPE_STATUS_BAR(2000) | TYPE_KEYGUARD(2002) | TYPE_SYSTEM_ALERT(2003) | TYPE_APPLICATION_OVERLAY(2038) 最高层级,不依赖Activity,需要系统权限 Z-order: 2000 ~ 2999 子窗口(1000 ~ 1999) TYPE_APPLICATION_PANEL(1000) | TYPE_APPLICATION_MEDIA(1001) | TYPE_APPLICATION_SUB_PANEL(1002) 必须依附父窗口,Z-order在父窗口之上 Z-order: 1000 ~ 1999 应用窗口(1 ~ 99) TYPE_APPLICATION(1) | TYPE_APPLICATION_STARTING(3) | TYPE_APPLICATION_ATTACHED_DIALOG(9) 必须依附Activity,生命周期跟随Activity Z-order: 1 ~ 99

从这张图可以清楚地看到,三个层级区间是严格隔离的。系统窗口在最顶层,子窗口在中间,应用窗口在最底层。每个区间内部,Type值越大,层级越高。

5.7 实战:如何选择合适的Window Type?

说了这么多理论,我们来点实际的。你在开发中该怎么选Type?我总结了一个简单的决策流程:

  1. 你的窗口需要跟随Activity吗? 是 → 用应用窗口(Type=1)
  2. 你的窗口是某个View的附属吗? 是 → 用子窗口(Type=1000~1002)
  3. 你的窗口需要全局显示吗? 是 → 用系统窗口
  4. 系统窗口里,需要显示在状态栏之上吗? 是 → 用TYPE_SYSTEM_ALERT(已废弃)或系统级Type
  5. 只是普通悬浮窗? 用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选对了没有——八成问题就出在这里。


公众号:蓝海资料掘金营,微信deep3321