窗口 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 里窗口排序的完整流程。

WMS 窗口 Z-order 排序流程 1. 收集所有可见窗口 2. 按窗口类型分组 3. 计算基础 Layer 值 4. 叠加子窗口/动画偏移 5. 焦点窗口 Layer +1000 6. 按 Layer 升序排列 遮挡关系示例 Layer 30000: 系统覆盖层 (最上) Layer 20000: 系统窗口 Layer 10000: 子窗口 Layer 1: 应用窗口 (最下)

这个图把整个流程串起来了。你注意看左侧的遮挡关系示例,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,性能开销不小。我给大家几个优化建议:

  1. 减少不必要的窗口层级:能用普通窗口就别用系统窗口,层级越深,计算越慢。
  2. 批量更新 Layer:如果多个窗口同时变化,尽量合并成一次排序,不要逐个触发。
  3. 缓存计算结果:对于不变化的窗口,缓存它们的 Layer 值,避免重复计算。

个人经验:我在优化启动速度时发现,WMS 在冷启动阶段会频繁调用 assignLayersLocked(),每次都要遍历几百个窗口。后来我加了个脏标记,只有窗口列表发生变化时才重新计算,启动时间直接降了 15%。

总结

Z-order 排序说白了就是两件事:算 Layer比大小。Layer 值由窗口类型、子窗口偏移、动画调整、焦点状态共同决定。理解了这个逻辑,你就能解释为什么有些窗口会莫名其妙被遮挡,为什么动画窗口总能显示在最上面。

嗯,今天的内容就到这里。记住,窗口排序不是玄学,是数学。Layer 值算对了,遮挡关系自然就对了。


公众号:蓝海资料掘金营,微信deep3321