SystemUI 中的 WindowManager 应用:状态栏、导航栏、锁屏、通知面板的窗口类型与层级管理
各位同学,今天我们来聊聊 SystemUI 和 WindowManager 的“联姻”。说实话,SystemUI 是 Android 系统里最“显眼”的进程——你每天解锁手机、看时间、调音量、下拉通知,全都在跟它打交道。而这一切的背后,都是 WindowManager 在默默调度窗口的层级和类型。
我个人习惯把 SystemUI 比作一个“窗口杂技团”。状态栏、导航栏、锁屏、通知面板,每个演员都有自己的窗口类型和层级位置。排错了,轻则遮挡内容,重则系统崩溃。嗯,咱们今天就拆开看看,这些“演员”到底是怎么登台的。
1. 状态栏:TYPE_STATUS_BAR 的“钉子户”
状态栏,就是屏幕顶部显示时间、信号、电量的那条。它的窗口类型是 TYPE_STATUS_BAR,对应的层级值在 WindowManager 里是 FIRST_STATUS_BAR_LAYER 到 LAST_STATUS_BAR_LAYER 之间。
为什么说它是“钉子户”?因为它的层级非常高,仅次于系统警告弹窗。我曾在项目中遇到过一个问题:第三方应用想通过悬浮窗盖住状态栏,结果死活盖不住。原因很简单——TYPE_STATUS_BAR 的层级比 TYPE_APPLICATION_OVERLAY 高太多了。
另外,状态栏还有一个特殊属性:它是“系统级”窗口,由 SystemUI 进程直接创建,不经过应用层的 Activity 生命周期。这意味着即使前台应用崩溃了,状态栏依然坚挺。你想想看,如果手机死机了连状态栏都消失,那用户得多慌?
2. 导航栏:TYPE_NAVIGATION_BAR 的“三键客”
导航栏,就是底部的返回键、Home 键、多任务键。它的窗口类型是 TYPE_NAVIGATION_BAR,层级略低于状态栏,但依然远高于普通应用。
这里有个细节:导航栏的窗口层级是 FIRST_NAVIGATION_BAR_LAYER 到 LAST_NAVIGATION_BAR_LAYER。为什么比状态栏低?因为导航栏在底部,用户手指操作时可能会误触到应用内容,所以给它留了一点“让步”空间。
我记得有一次调试一个全屏游戏,游戏想隐藏导航栏来获得沉浸式体验。结果隐藏后,用户从底部上滑时,导航栏重新出现,但动画卡顿。排查下来发现是导航栏窗口的 FLAG_NOT_FOCUSABLE 和 FLAG_ALT_FOCUSABLE_IM 标志位没处理好,导致输入法窗口跟它抢焦点。嗯,这种坑,不踩一次真的记不住。
3. 锁屏:TYPE_KEYGUARD 的“守门员”
锁屏,也就是 Keyguard,它的窗口类型是 TYPE_KEYGUARD。这个类型的层级非常特殊——它介于状态栏和导航栏之间,但有一个关键行为:锁屏窗口会“接管”整个屏幕的触摸事件。
为什么需要这样?因为锁屏状态下,用户不应该能操作应用。所以 WindowManager 给 TYPE_KEYGUARD 窗口设置了 FLAG_NOT_TOUCH_MODAL 和 FLAG_WATCH_OUTSIDE_TOUCH 标志,确保所有触摸事件先经过锁屏窗口过滤。
我遇到过最头疼的一个 bug:锁屏状态下,用户从顶部下滑,状态栏居然能拉下来。排查后发现是锁屏窗口的 TYPE_KEYGUARD 类型被错误地改成了 TYPE_STATUS_BAR_PANEL,导致层级错乱。说白了,窗口类型就是身份证,乱改就会出大问题。
| 窗口类型 | 层级范围 | 主要用途 |
|---|---|---|
| TYPE_STATUS_BAR | FIRST_STATUS_BAR_LAYER ~ LAST_STATUS_BAR_LAYER | 状态栏显示 |
| TYPE_NAVIGATION_BAR | FIRST_NAVIGATION_BAR_LAYER ~ LAST_NAVIGATION_BAR_LAYER | 导航栏显示 |
| TYPE_KEYGUARD | FIRST_KEYGUARD_LAYER ~ LAST_KEYGUARD_LAYER | 锁屏界面 |
| TYPE_STATUS_BAR_PANEL | FIRST_STATUS_BAR_PANEL_LAYER ~ LAST_STATUS_BAR_PANEL_LAYER | 通知面板、快速设置 |
4. 通知面板:TYPE_STATUS_BAR_PANEL 的“下拉菜单”
通知面板,就是你从顶部下拉出来的那个大面板。它的窗口类型是 TYPE_STATUS_BAR_PANEL,层级在状态栏之上。
为什么要在状态栏之上?因为通知面板是从状态栏拉出来的,它需要覆盖状态栏本身。你想想看,如果通知面板在状态栏下面,那下拉后状态栏还露在外面,多难看?
这里有一个重要的层级管理逻辑:通知面板的窗口是“临时”的。它只在用户下拉时创建,收起时销毁。但注意,不是真的销毁,而是通过 hide() 方法隐藏。为什么?因为频繁创建销毁窗口会导致性能问题,尤其是动画过程中。
TYPE_STATUS_BAR_PANEL 类型有一个隐含要求:它必须有一个父窗口,通常是状态栏窗口。如果没有正确设置父窗口,面板的动画会跟状态栏脱节,出现“面板飞走了”的诡异现象。我曾经在定制 ROM 时踩过这个坑,排查了整整两天。
5. 层级管理的核心逻辑:z-order 排序
说了这么多类型,其实底层就一个逻辑:z-order 排序。WindowManager 在添加窗口时,会根据窗口类型计算一个 z-order 值,然后插入到窗口列表的对应位置。
这个排序规则大致是这样的:
- 应用窗口(TYPE_APPLICATION):z-order 最低,1~99
- 子窗口(TYPE_APPLICATION_PANEL、TYPE_APPLICATION_MEDIA 等):z-order 在父窗口之上
- 系统窗口(TYPE_STATUS_BAR、TYPE_NAVIGATION_BAR 等):z-order 在 2000 以上
- 系统警告窗口(TYPE_SYSTEM_ALERT、TYPE_SYSTEM_ERROR 等):z-order 最高,通常在 3000 以上
但这里有个细节:同类型的窗口之间,后添加的会覆盖先添加的。所以 SystemUI 在启动时,必须按照正确的顺序添加状态栏、导航栏、锁屏等窗口。顺序错了,界面就会乱套。
我建议你在阅读 SystemUI 源码时,重点关注 BaseStatusBar.java 中的 start() 方法。那里就是窗口创建的入口,顺序一目了然。
6. 一张图看懂层级关系
说了这么多,不如一张图来得直观。下面这张 SVG 图展示了 SystemUI 各窗口的层级关系:
从这张图可以清晰地看到:状态栏在最顶层,锁屏在中间层,导航栏在底层。通知面板虽然没画出来,但它其实在状态栏之上——因为下拉时它需要覆盖状态栏。
7. 实战中的坑与经验
最后,分享几个我在实际项目中踩过的坑:
- 窗口类型冲突:有一次第三方应用用了
TYPE_SYSTEM_ALERT来显示悬浮球,结果跟 SystemUI 的导航栏抢层级,导致导航栏点击无响应。解决方案是限制第三方应用使用系统窗口类型。 - 动画卡顿:通知面板下拉动画卡顿,排查发现是窗口的
FLAG_HARDWARE_ACCELERATED没开。加上后流畅很多。 - 锁屏穿透:锁屏状态下,用户通过某些手势能操作后台应用。这是因为锁屏窗口的
FLAG_NOT_TOUCH_MODAL设置不当,导致触摸事件穿透了。
嗯,今天就先聊到这里。SystemUI 的窗口管理远不止这些,但理解了状态栏、导航栏、锁屏、通知面板这四大金刚的层级关系,你就掌握了核心骨架。剩下的细节,咱们后面慢慢填。
公众号:蓝海资料掘金营,微信deep3321