12、WMS 中的窗口排序:Z-Order 的计算逻辑与 Layer 分配
窗口排序,说白了就是决定「谁盖在谁上面」。
你想想看,手机屏幕上那么多窗口——状态栏、导航栏、应用界面、Dialog、Toast、输入法……它们怎么知道谁该在最前面?这就是 Z-Order 要解决的问题。
我在做 Android 多窗口开发时,遇到过好几次窗口层级错乱的问题。有一次是悬浮窗死活盖不住输入法,查了两天才发现是 Z-Order 计算时少加了一个 Layer。嗯,从那以后我对这块就特别敏感。
12.1 什么是 Z-Order?
Z-Order 就是窗口在 Z 轴(垂直于屏幕的方向)上的排序值。值越大,窗口越靠上。
WMS 里用 WindowState.mBaseLayer 和 WindowState.mSubLayer 两个字段来共同决定一个窗口的最终 Z 值。
核心公式:
最终 Z-Order = mBaseLayer + mSubLayer
其中 mBaseLayer 由窗口类型决定,mSubLayer 用于同一窗口内的子窗口排序。
12.2 窗口类型与 BaseLayer 的分配
WMS 里定义了一套窗口类型常量,每个类型对应一个固定的 BaseLayer 范围。我习惯把它们分成三大类:
| 窗口类型 | BaseLayer 范围 | 典型窗口 |
|---|---|---|
| 应用窗口 | 1 ~ 9999 | Activity 主窗口 |
| 子窗口 | 10000 ~ 19999 | PopupWindow、Dialog |
| 系统窗口 | 20000 ~ 29999 | 状态栏、导航栏、输入法 |
具体到代码里,WMS 在 WindowManagerService.java 中有一个 getWindowLayerLw() 方法,它会根据窗口的 type 返回对应的 Layer 值。
// 简化后的逻辑
int getWindowLayerLw(int windowType) {
switch (windowType) {
case TYPE_STATUS_BAR:
return 20000; // 状态栏
case TYPE_NAVIGATION_BAR:
return 21000; // 导航栏
case TYPE_INPUT_METHOD:
return 22000; // 输入法
case TYPE_APPLICATION:
return 1; // 普通应用
// ... 其他类型
}
}
这里有个细节:同类型的窗口之间,BaseLayer 是相同的。那它们之间怎么排序?靠的是 WMS 内部的窗口列表顺序。
12.3 SubLayer:子窗口的精细控制
一个窗口可以拥有多个子窗口,比如一个 Activity 里弹出了 PopupWindow,又弹出了 Dialog。这些子窗口之间也需要排序。
SubLayer 的值范围是 -2 到 2,具体含义如下:
- -2:子窗口在父窗口下方(极少用)
- -1:子窗口在父窗口下方紧邻
- 0:与父窗口同级(默认值)
- 1:子窗口在父窗口上方紧邻
- 2:子窗口在父窗口上方(常用于 Dialog)
我的经验: 如果你自己实现悬浮窗,记得把 SubLayer 设为 1 或 2。我曾经设成 0,结果悬浮窗和父窗口抢层级,点击事件全乱了。
12.4 Z-Order 的计算流程
WMS 在每次窗口添加或移除时,都会重新计算所有窗口的 Z-Order。流程大致如下:
- 确定 BaseLayer:根据窗口类型调用
getWindowLayerLw() - 确定 SubLayer:如果是子窗口,从
WindowManager.LayoutParams中读取 - 计算最终 Z 值:BaseLayer + SubLayer
- 插入窗口列表:按 Z 值从小到大排序,插入到
mWindows列表中 - 通知 SurfaceFlinger:将排序后的列表传给 SurfaceFlinger,由它负责实际渲染
这里我画了一张流程图,帮你理清整个计算过程:
12.5 Layer 分配的实际案例
咱们来看一个具体场景。假设屏幕上同时有:
- 一个 Activity(type = TYPE_APPLICATION)
- 一个 PopupWindow(子窗口,SubLayer = 1)
- 状态栏(type = TYPE_STATUS_BAR)
- 输入法(type = TYPE_INPUT_METHOD)
它们的 Z-Order 计算如下:
| 窗口 | BaseLayer | SubLayer | 最终 Z | 渲染顺序(从下到上) |
|---|---|---|---|---|
| Activity | 1 | 0 | 1 | 1(最底层) |
| PopupWindow | 1 | 1 | 2 | 2 |
| 状态栏 | 20000 | 0 | 20000 | 3 |
| 输入法 | 22000 | 0 | 22000 | 4(最顶层) |
你看,输入法盖住了状态栏,状态栏盖住了 PopupWindow,PopupWindow 盖住了 Activity。这就是我们日常看到的屏幕层级。
注意: 这里有个坑。如果 PopupWindow 的 SubLayer 设成了 2,而 Dialog 的 SubLayer 也是 2,那它们之间谁盖谁?答案是:看它们在 WMS 窗口列表中的插入顺序。后插入的会盖在先插入的上面。
我曾经遇到过两个悬浮窗互相抢层级,就是因为 SubLayer 设成了相同的值。后来我给每个悬浮窗分配了不同的 SubLayer 偏移量,问题才解决。
12.6 与 SurfaceFlinger 的交互
WMS 算完 Z-Order 后,会把结果传给 SurfaceFlinger。SurfaceFlinger 根据 Z 值决定每个 Layer 的绘制顺序。
这里有个关键点:WMS 传给 SurfaceFlinger 的是排序后的窗口列表,而不是 Z 值本身。SurfaceFlinger 按照列表顺序从下往上绘制,后绘制的自然覆盖先绘制的。
// WMS 中通知 SurfaceFlinger 的简化代码
void updateSurfaceFlinger() {
// 按 Z-Order 排序后的窗口列表
ArrayList<WindowState> sortedWindows = mWindows;
// 遍历列表,逐个更新 Layer
for (int i = 0; i < sortedWindows.size(); i++) {
WindowState w = sortedWindows.get(i);
w.mSurfaceController.setLayer(i); // 这里的 i 就是 Z 值
}
}
嗯,这里要注意:setLayer(i) 中的 i 是列表中的索引,而不是 BaseLayer 的值。所以即使两个窗口的 BaseLayer 相差很大,只要它们在列表中的位置相邻,渲染时就是紧挨着的。
12.7 避坑指南
最后分享几个我踩过的坑:
- 不要随意修改窗口类型:我曾经为了省事,把悬浮窗的类型设成了 TYPE_SYSTEM_ALERT,结果它直接盖住了状态栏,用户关都关不掉。后来老老实实用了 TYPE_APPLICATION_OVERLAY。
- 注意 Android 版本差异:Android 10 之后,TYPE_PHONE 等旧类型被废弃,改用 TYPE_APPLICATION_OVERLAY。如果你还在用旧类型,系统会直接忽略你的窗口。
- SubLayer 不是万能的:它只对同一个父窗口下的子窗口有效。不同父窗口之间的排序,还是靠 BaseLayer。
- 调试技巧:用
adb shell dumpsys window windows可以查看当前所有窗口的 Z-Order 值。我排查层级问题时,第一件事就是跑这个命令。
窗口排序这块,说白了就是「类型决定大层级,子窗口决定小层级」。理解了 BaseLayer 和 SubLayer 的关系,你就能掌控屏幕上任何一个窗口的显示顺序。
个人习惯: 我每次写自定义窗口时,都会先在纸上画一下层级关系图。哪个窗口该在哪个上面,一目了然。代码写起来也顺手很多。
公众号:蓝海资料掘金营,微信deep3321