WMS与Input系统响应优化实践 - 第2节
一、窗口层级:谁在上面?凭什么?
其实窗口层级这个东西,说白了就是系统用来决定“谁在上面谁在下面”的一套规则。你想想看,手机屏幕上那么多东西——状态栏、输入法、弹窗、应用界面——它们不可能乱糟糟堆在一起吧?
我记得刚接触WMS源码时,看到那一堆type定义,头都大了。后来我才明白,窗口层级本身就是一个int值,值越大,窗口越靠上。
具体来说,窗口类型被分成几个大类:
| 类型范围 | 典型窗口 | 层级范围 |
|---|---|---|
| 应用窗口 | Activity、Dialog | 1 - 99 |
| 子窗口 | PopupWindow、菜单 | 1000 - 1999 |
| 系统窗口 | 状态栏、导航栏、输入法 | 2000 - 2999 |
嗯,这里要注意。光有这个范围还不够,真正决定窗口顺序的,是WMS内部的mWindowMap和mOrderedWindowLists。每个窗口在添加时,会计算一个“全局层序值”,然后插入到合适的位置。
1.1 窗口层级的计算逻辑
每个窗口在WMS里都会分配一个mBaseLayer和一个mSubLayer。前者由窗口类型决定,后者用于子窗口的微调。
// 伪代码示意
int baseLayer = type * 10000 + 1000; // 不同类型拉开差距
int subLayer = (attrs.type == TYPE_APPLICATION) ? 0 : attrs. != null ? 1 : -1;
int finalLayer = baseLayer + subLayer; // 最终排序依据
为什么乘以10000?说白了就是给不同类型之间留足空间。我在项目中就遇到过一个问题:两个系统窗口同时弹出,其中一个把另一个盖住了。后来一查,原来是它们的type算出来的baseLayer恰好相差不大,加上subLayer后又发生了冲突。
1.2 层高 vs. Z-order
你可能听说过 Z-order。它其实是窗口在WMS内部列表中的索引位置。索引越小,越靠下;索引越大,越靠上。
但Z-order和type不完全是一回事。举个例子:
- 一个
TYPE_APPLICATION的对话框,type是1。 - 一个
TYPE_SYSTEM_ALERT的悬浮窗,type是2003。
按常理,2003 > 1,所以悬浮窗总在对话框上面。但如果你设置了FLAG_NOT_FOCUSABLE或其他标志位,触摸事件就可能穿越过去。Z-order管显示顺序,但不完全管触摸响应顺序——这部分后面章节会细说。
FLAG_NOT_TOUCHABLE,导致触摸事件无法穿透。解决方案是在弹出前调用removeView()清理掉那个窗口。
二、窗口:身份证明与权限校验
每个窗口必须有一个,否则WMS会直接拒绝添加。到底是什么?
我刚开始接触WMS源码时,看到WindowToken这个类,第一反应是:这不就是个Binder对象吗?对,也不全对。
主要有两种:
- 应用(AppWindowToken):由ActivityManagerService创建,关联一个特定的Activity。每个Activity只能有一个AppWindowToken。
- 非应用(WindowToken):用于系统窗口、输入法窗口等。不需要AMS参与,WMS自己就能创建。
2.1 的作用
- 权限校验:系统窗口必须有对应的,否则不被允许。这样可以防止恶意应用随意弹出覆盖窗口。
- 窗口分组:同一个下的窗口属于同一组。例如一个Activity的Dialog和它的主窗口共享同一个AppWindowToken,所以Dialog会在Activity上方、而不会跑到状态栏下面去。
- 生命周期管理:当Activity销毁时,WMS会根据AppWindowToken找到所有关联窗口,一并移除。
// WMS.addWindow() 中的关键校验
if (windowToken == null) {
if (type >= FIRST_SYSTEM_WINDOW) {
throw new WindowManager.BadTokenException("Unable to add window -- system window without ");
}
// 非系统窗口可以自动创建临时
windowToken = createToken(type, ...);
}
看到没?系统窗口如果没有,直接抛异常。我在项目中就踩过这个坑:第三方SDK弹了一个悬浮窗,结果在Android 10上直接崩溃,就是因为为空。
TYPE_APPLICATION_OVERLAY直接获取显示权限,必须先通过ACTION_MANAGE_OVERLAY_PERMISSION申请。这是个典型的权限收紧案例。
三、布局参数:窗口的尺寸、位置与行为
布局参数就是WindowManager.LayoutParams。我习惯称之为“窗口的配置清单”。它包含几十个字段,但真正用得多的,也就下面这些:
| 字段 | 作用 | 常用值 |
|---|---|---|
type |
窗口类型 | TYPE_APPLICATION, TYPE_SYSTEM_ALERT |
flags |
行为标志位 | FLAG_NOT_FOCUSABLE, FLAG_KEEP_SCREEN_ON |
width/height |
窗口尺寸 | MATCH_PARENT, WRAP_CONTENT, 具体像素 |
gravity |
窗口位置对齐 | Gravity.CENTER, Gravity.TOP |
x/y |
窗口偏移量 | 正数向右/下偏移 |
softInputMode |
软键盘行为 | ADJUST_RESIZE, ADJUST_PAN |
|
关联的 | Activity的IBinder |
3.1 flags:你真的会组合吗?
flag其实是个int,每一位代表一个开关。比如:
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
如果既想窗口不可触摸,又要保持屏幕常亮,可以这样组合。但要注意,FLAG_NOT_FOCUSABLE和FLAG_NOT_TOUCH_MODAL的区别:前者完全不接收焦点,触摸事件会传递到下层窗口;后者只拦截当前窗口内的触摸,窗口外的触摸会穿透。
我印象很深的一次:一个锁屏音乐播放器在Android 9上总是无法接收到按键事件。排查下来,是FLAG_NOT_FOCUSABLE被错误地设为了true,导致KeyEvent全都被上层状态栏拦截了。改成FLAG_NOT_TOUCH_MODAL后问题解决。
3.2 gravity和x/y的配合
很多人以为设置了gravity就万事大吉了。其实x和y是在gravity确定的位置基础上再偏移。举个例子:
params.gravity = Gravity.CENTER | Gravity.TOP; // 先水平居中、顶部对齐
params.x = 50; // 再向右偏移50px
params.y = 20; // 再向下偏移20px
最终窗口的位置是:水平居中 + 50px,顶部对齐 + 20px。如果没理解这个叠加关系,很容易把窗口放到屏幕之外。
onViewAdded回调里打印出getAttachedInfo(),看看WMS最终计算出来的Frame是多少。如果跟预期不符,一步步检查gravity、x/y和父容器的尺寸限制。
本章核心结构图
下面这张图展示了窗口层级的构成、的验证流程以及布局参数的分配逻辑。我习惯把它画成一条线:从WindowManager添加窗口开始,经过WMS校验、计算层级、解析布局参数,最终生成Surface并显示。
这张图我画得比较紧凑。从顶部WMS的addWindow()入口开始,向下展开三条核心分支:左边是校验,中间是层级计算,右边是布局参数解析。三者都通过后,最终走到Surface的生成和窗口列表的插入。
实际调试时,你可以通过dumpsys window看到每一个窗口的状态,包括它的、层级索引、布局参数等。我个人建议把这个命令记熟了,面试和排查问题都特别有用。
type字段。嗯,从那以后我每次写addView都会把LayoutParams的每个字段过一遍。