12、WMS 中的窗口排序:Z-Order 的计算逻辑与 Layer 分配

窗口排序,说白了就是决定「谁盖在谁上面」。

你想想看,手机屏幕上那么多窗口——状态栏、导航栏、应用界面、Dialog、Toast、输入法……它们怎么知道谁该在最前面?这就是 Z-Order 要解决的问题。

我在做 Android 多窗口开发时,遇到过好几次窗口层级错乱的问题。有一次是悬浮窗死活盖不住输入法,查了两天才发现是 Z-Order 计算时少加了一个 Layer。嗯,从那以后我对这块就特别敏感。

12.1 什么是 Z-Order?

Z-Order 就是窗口在 Z 轴(垂直于屏幕的方向)上的排序值。值越大,窗口越靠上。

WMS 里用 WindowState.mBaseLayerWindowState.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。流程大致如下:

  1. 确定 BaseLayer:根据窗口类型调用 getWindowLayerLw()
  2. 确定 SubLayer:如果是子窗口,从 WindowManager.LayoutParams 中读取
  3. 计算最终 Z 值:BaseLayer + SubLayer
  4. 插入窗口列表:按 Z 值从小到大排序,插入到 mWindows 列表中
  5. 通知 SurfaceFlinger:将排序后的列表传给 SurfaceFlinger,由它负责实际渲染

这里我画了一张流程图,帮你理清整个计算过程:

窗口添加/移除事件 步骤1: 获取窗口类型 (type) 步骤2: 调用 getWindowLayerLw() 得到 BaseLayer 步骤3: 判断是否有子窗口? 有则读取 SubLayer 步骤4: 计算最终 Z = BaseLayer + SubLayer 步骤5: 插入 mWindows 列表,通知 SurfaceFlinger TYPE_APPLICATION → 1 TYPE_STATUS_BAR → 20000 TYPE_INPUT_METHOD → 22000 SubLayer: -2 ~ 2

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