窗口 Z-order 排序算法:WMS 中窗口的排序规则、Layer 值的计算、窗口遮挡与显示优先级
大家好,我是你们的老朋友。今天我们来聊聊窗口管理里一个特别有意思的话题——Z-order 排序。说白了,就是决定哪个窗口在上面、哪个在下面。你想想看,手机屏幕上那么多窗口叠在一起,总得有个先来后到吧?
我在做 Android 系统开发的头两年,其实没太在意这个排序逻辑。直到有一次,一个第三方应用弹了个全屏对话框,把系统状态栏给盖住了,用户怎么划都划不下来。嗯,从那以后,我就把 Z-order 这块啃了个透。
Z-order 的基本概念
Z-order 这个词,说白了就是三维空间里的 Z 轴顺序。屏幕是 XY 平面,Z 轴指向你的眼睛。Z 值越大,窗口离你越近,显示优先级越高。
WMS 里维护着一个全局的窗口列表,这个列表的顺序就是 Z-order 的体现。列表末尾的窗口在最上面,开头的在最下面。为什么是末尾?因为 WMS 在遍历绘制时,是从头到尾依次绘制的,后绘制的自然覆盖先绘制的。
核心原则:Z-order 值越大 → 窗口越靠上 → 越先被用户看到 → 越晚被绘制。
Layer 值的计算规则
每个窗口在 WMS 里都有一个 mLayer 字段,这个值就是它的 Z-order 数值。计算规则其实不复杂,我给大家拆解一下。
基础 Layer 值
WMS 为每种窗口类型定义了一个基础 Layer 范围。比如:
| 窗口类型 | 基础 Layer 范围 | 典型示例 |
|---|---|---|
| 应用窗口 | 1 ~ 9999 | 普通 Activity |
| 子窗口 | 10000 ~ 19999 | PopupWindow、Dialog |
| 系统窗口 | 20000 ~ 29999 | 状态栏、导航栏 |
| 系统覆盖层 | 30000 ~ 39999 | Toast、输入法 |
这个范围划分,说白了就是给不同优先级的窗口划了地盘。系统窗口永远比应用窗口高,这是硬性规定。
Layer 值的叠加计算
实际 Layer 值不是简单的基础值,而是叠加了多个因素。我记得 WMS 里有个方法叫 assignLayersLocked(),里面做了这么几件事:
// 伪代码,展示核心逻辑
void assignLayersLocked(WindowList windows) {
int layer = 0;
for (WindowState w : windows) {
// 1. 基础类型层
layer += getBaseLayer(w.mAttrs.type);
// 2. 子窗口偏移
if (w.mAttrs.type == TYPE_APPLICATION_SUB_PANEL) {
layer += 10000;
}
// 3. 窗口动画偏移
if (w.mWinAnimator != null) {
layer += w.mWinAnimator.mAnimLayerAdjustment;
}
// 4. 焦点窗口提升
if (w == mFocusedWindow) {
layer += 1000;
}
w.mLayer = layer;
layer += 1; // 同类型窗口间递增
}
}
这里有个细节:同类型窗口之间,后添加的 Layer 值更大。所以如果你连续打开两个普通 Activity,后打开的那个会盖住前一个。
避坑指南:我曾经遇到过一个 bug,某个应用连续弹了两个 Dialog,结果第二个 Dialog 被第一个盖住了。查了半天发现是子窗口的 Layer 计算没处理好,子窗口的 parent 窗口 Layer 变化时,子窗口没有跟着重新计算。解决方案是在 relayoutWindow() 里强制刷新子窗口的 Layer。
窗口遮挡与显示优先级
有了 Layer 值,遮挡关系就很好判断了。但实际场景比理论复杂得多。我给大家画个图,看看 WMS 里窗口排序的完整流程。
这个图把整个流程串起来了。你注意看左侧的遮挡关系示例,Layer 值越大,窗口越靠上。系统覆盖层永远在最上面,应用窗口在最下面。
特殊场景的处理
实际项目中,有几个特殊场景需要额外注意。我挑两个典型的说说。
场景一:窗口动画期间的 Z-order 变化
窗口在打开或关闭时,会有一个动画过程。这时候 WMS 会临时调整 Layer 值,让动画窗口显示在最上面。动画结束后再恢复。我记得在 WindowStateAnimator 里有个 mAnimLayerAdjustment 字段,就是干这个的。
// 窗口打开动画时,临时提升 Layer
void setAnimLayerAdjustment(int adj) {
mAnimLayerAdjustment = adj;
// 通知 WMS 重新计算 Layer
mService.scheduleAnimationLocked();
}
这个调整值通常是 1000 或 2000,确保动画窗口不会被其他窗口遮挡。动画结束后,调整值归零,窗口回到正常层级。
注意:如果动画窗口的 parent 窗口被销毁了,子窗口的动画调整值必须同步清零。否则会出现「幽灵窗口」——窗口明明关闭了,但 Layer 值还很高,导致其他窗口无法显示。我曾经在 Android 8.0 上修过这个 bug,排查了两天才找到根因。
场景二:多窗口模式下的 Z-order
分屏或自由窗口模式下,Z-order 的计算会更复杂。每个窗口栈(Stack)有自己的 Layer 范围,栈之间还有优先级。比如:
- 全屏栈:Layer 范围 1 ~ 10000
- 分屏栈:Layer 范围 10001 ~ 20000
- 自由窗口栈:Layer 范围 20001 ~ 30000
全屏栈的窗口永远在分屏栈下面?不一定。如果分屏栈的窗口获得了焦点,WMS 会把它所在的整个栈的 Layer 提升到全屏栈之上。这就是「焦点栈优先」原则。
性能优化建议
Z-order 排序虽然逻辑不复杂,但每次窗口变化都要重新计算所有窗口的 Layer,性能开销不小。我给大家几个优化建议:
- 减少不必要的窗口层级:能用普通窗口就别用系统窗口,层级越深,计算越慢。
- 批量更新 Layer:如果多个窗口同时变化,尽量合并成一次排序,不要逐个触发。
- 缓存计算结果:对于不变化的窗口,缓存它们的 Layer 值,避免重复计算。
个人经验:我在优化启动速度时发现,WMS 在冷启动阶段会频繁调用 assignLayersLocked(),每次都要遍历几百个窗口。后来我加了个脏标记,只有窗口列表发生变化时才重新计算,启动时间直接降了 15%。
总结
Z-order 排序说白了就是两件事:算 Layer 和 比大小。Layer 值由窗口类型、子窗口偏移、动画调整、焦点状态共同决定。理解了这个逻辑,你就能解释为什么有些窗口会莫名其妙被遮挡,为什么动画窗口总能显示在最上面。
嗯,今天的内容就到这里。记住,窗口排序不是玄学,是数学。Layer 值算对了,遮挡关系自然就对了。
公众号:蓝海资料掘金营,微信deep3321