Window 的概念与分类:应用窗口、子窗口、系统窗口的区别与使用场景

大家好,我是你们的老朋友。今天我们来聊聊 Window 的分类。

说实话,Window 这个概念,很多同学刚接触时容易混淆。它不像 Activity 那么直观,但又是 Android 显示体系里最核心的一环。我个人习惯把 Window 理解成「一块画板」,所有 View 都要挂在这块画板上才能显示。而这块画板,根据它的「出身」和「用途」,可以分为三大类:应用窗口、子窗口、系统窗口。

1. 应用窗口(Application Window)

应用窗口,说白了就是每个 App 默认的主窗口。它对应一个 Activity,也是我们最常打交道的窗口类型。

特点:

  • 必须绑定一个 Activity。
  • 拥有独立的 WindowManager.LayoutParams,type 范围在 FIRST_APPLICATION_WINDOWLAST_APPLICATION_WINDOW(1 ~ 99)。
  • 通常占据全屏或部分屏幕区域。

使用场景:

  • App 的主界面、弹窗(Dialog 其实也是应用窗口的一种变体)。
  • 启动页、主页面、设置页等。

核心要点:应用窗口的 token 必须来自 Activity。没有 token,窗口就挂不上。我在项目中遇到过新手直接把 Dialog 的 window type 设成系统窗口,结果崩溃了——嗯,token 不对。

2. 子窗口(Sub Window)

子窗口,顾名思义,它不能独立存在,必须依附于一个父窗口。你可以把它想象成「画板上的小画板」。

特点:

  • type 范围在 FIRST_SUB_WINDOWLAST_SUB_WINDOW(1000 ~ 1999)。
  • 必须有一个有效的父窗口 token。
  • 它的显示层级受父窗口影响,但可以浮动在父窗口之上。

使用场景:

  • PopupWindow:最常见的子窗口实现。
  • ContextMenu、Spinner 下拉列表。
  • 某些自定义的 Tooltip 或悬浮菜单。

避坑指南:我曾经在项目里用 PopupWindow 做下拉筛选,结果在部分机型上点击外部无法关闭。后来发现是父窗口 token 传递出了问题。记住:子窗口的 token 必须来自父窗口的 getWindowToken(),而不是 getApplicationToken()

3. 系统窗口(System Window)

系统窗口,这是权限最高的一类窗口。它不依赖任何 Activity,可以显示在所有应用之上。说白了,它就是「全局浮层」。

特点:

  • type 范围在 FIRST_SYSTEM_WINDOWLAST_SYSTEM_WINDOW(2000 ~ 2999)。
  • 需要申请 SYSTEM_ALERT_WINDOW 权限(悬浮窗权限)。
  • 显示层级最高,甚至可以覆盖状态栏和导航栏。

使用场景:

  • 悬浮球、手机管家类 App 的浮窗。
  • 来电界面、闹钟提醒。
  • 系统级 Toast(非应用内 Toast)。

注意:系统窗口滥用会导致用户体验极差。我记得 Google 在 Android 10 以后对系统窗口做了严格限制,后台启动的 Activity 和窗口都被限制了。如果你要做悬浮窗,一定要处理好生命周期和权限申请。

4. 三种窗口的对比

为了让你更直观地理解它们的区别,我整理了一张表:

维度 应用窗口 子窗口 系统窗口
type 范围 1 ~ 99 1000 ~ 1999 2000 ~ 2999
依赖 Activity 是(依赖父窗口)
显示层级 中(在父窗口之上) 高(全局最上层)
权限要求 SYSTEM_ALERT_WINDOW
典型代表 Activity、Dialog PopupWindow、Menu 悬浮窗、系统 Toast

5. 核心逻辑图:Window 分类与层级关系

下面我用一张 SVG 图来展示这三种窗口在系统中的位置关系。你想想看,窗口层级就像一栋楼:应用窗口在一楼,子窗口在二楼(但必须踩着父窗口的地板),系统窗口在顶楼,可以俯瞰一切。

Android 窗口分类与层级关系 系统窗口(System Window) type: 2000~2999 | 需要悬浮窗权限 | 全局最顶层 典型:悬浮球、来电界面、系统 Toast 子窗口(Sub Window) type: 1000~1999 | 必须依附父窗口 token 典型:PopupWindow、ContextMenu、Spinner 下拉 应用窗口(Application Window) type: 1~99 | 绑定 Activity token 典型:Activity、Dialog 层级从下往上递增,系统窗口拥有最高显示优先级

6. 如何选择正确的窗口类型?

这个问题,我经常被问到。其实判断标准很简单:

  1. 你的 UI 是否属于某个 Activity? 如果是,用应用窗口或子窗口。
  2. 是否需要浮在别的 App 之上? 如果是,用系统窗口,但别忘了申请权限。
  3. 是否需要跟随父窗口移动? 如果是,用子窗口(比如 PopupWindow 会跟随锚点 View)。

我的经验:能不用系统窗口就不用。系统窗口不仅权限麻烦,而且在 Android 10+ 上限制很多。我曾经为了做一个全局悬浮球,折腾了三个版本才适配好。后来改用前台 Service + 系统窗口,才稳定下来。

7. 避坑总结

  • token 问题:应用窗口和子窗口都需要有效的 token。没有 token,WindowManager.addView() 会直接抛异常。
  • 权限问题:系统窗口在 Android 6.0+ 需要动态申请悬浮窗权限,且用户可以在设置中关闭。
  • 生命周期:子窗口随着父窗口销毁而销毁。如果你在 Activity 销毁后还尝试操作子窗口,会引发 WindowLeaked 崩溃。
  • type 值不要乱设:每个 type 都有对应的行为约束。比如 TYPE_APPLICATION_OVERLAY 在 Android 10 以后替代了旧的 TYPE_PHONE,如果你还用旧的,可能无法显示。

好了,关于 Window 的分类就讲到这里。这三种窗口类型,你只要记住它们的「出身」和「权限」,在实际开发中就不会用错。下一章我们会深入 WindowManager 的添加流程,看看 addView 到底做了什么。