27、窗口管理调试:WindowManager、窗口层级、焦点管理、多窗口问题

窗口管理,说白了就是Android系统里最核心的“显示调度中心”。

我经常跟团队里的新人说:你搞懂了窗口管理,就等于搞懂了半个Android系统。为什么?因为所有你看到的UI元素——Activity、Dialog、PopupWindow、Toast、输入法面板、状态栏——全都要经过WindowManager来排布和绘制。

今天我们就来聊聊窗口管理的调试方法。我会结合我这些年踩过的坑,把WindowManager、窗口层级、焦点管理、多窗口这几个核心话题讲透。

27.1 WindowManager 的核心职责

WindowManager是一个系统服务,它负责管理所有窗口的创建、更新、删除、Z-order排序、焦点分配、输入事件分发。

说白了,它就是Android显示系统的“总导演”。每个窗口都是一个演员,WindowManager决定谁在前、谁在后、谁获得观众的注意力(焦点)。

核心数据结构:WindowManagerService(WMS)内部维护了一个窗口列表,按Z-order从低到高排序。每个窗口都有一个WindowState对象,记录了窗口的所有状态信息。

我在项目中遇到过一个问题:某个App的弹窗总是显示在系统对话框下面。排查了半天,发现是窗口的type设置错了。嗯,这里要注意——窗口类型决定了它的层级位置。

27.2 窗口层级(Z-order)详解

窗口层级是调试中最容易出问题的地方。Android把窗口分成了几大类,每一类有固定的层级范围:

窗口类型 Type值 层级范围 典型用途
应用窗口 FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW 1 ~ 99 Activity主窗口
子窗口 FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW 1000 ~ 1999 PopupWindow、Dialog
系统窗口 FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW 2000 ~ 2999 状态栏、导航栏、输入法
系统错误窗口 TYPE_SYSTEM_ERROR 2010 ANR对话框、崩溃提示
系统覆盖窗口 TYPE_SYSTEM_OVERLAY 2013 悬浮窗、Toast

你想想看,为什么有些App的悬浮窗会被系统对话框挡住?就是因为TYPE_SYSTEM_OVERLAY(2013)的层级低于TYPE_SYSTEM_ERROR(2010)?不对,实际上2013比2010大,按理说应该在上面。但这里有个细节:系统错误窗口有特殊的优先级处理逻辑。

调试技巧:adb shell dumpsys window windows可以查看当前所有窗口的层级信息。输出里会显示每个窗口的mBaseLayermSubLayer,这两个值加起来就是最终的Z-order。

27.3 焦点管理调试

焦点管理是另一个容易让人头疼的问题。焦点决定了输入事件(触摸、按键)该发给哪个窗口。

WMS维护了一个“焦点窗口”的概念。当用户触摸屏幕时,WMS会找到触摸点所在的窗口,然后把事件发过去。但如果窗口没有获得焦点,它可能收不到键盘事件。

我曾经遇到一个Bug:用户按返回键,App没有响应。查了半天,发现是一个透明的系统窗口抢了焦点。透明窗口?对,它设置了FLAG_NOT_TOUCHABLE,但没设置FLAG_NOT_FOCUSABLE。结果就是:它挡住了所有按键事件。

避坑指南:创建透明覆盖窗口时,一定要同时设置FLAG_NOT_TOUCHABLEFLAG_NOT_FOCUSABLE。否则你会收获一堆“按键无响应”的投诉。

调试焦点问题,我习惯用这个命令:

adb shell dumpsys window focused

它会直接告诉你当前哪个窗口拥有焦点。如果输出是null,说明没有窗口获得焦点——这通常是个问题。

27.4 多窗口模式下的调试

Android从7.0开始支持多窗口,包括分屏、画中画、自由窗口。多窗口模式下,窗口管理的复杂度翻了好几倍。

核心变化有几点:

  • 窗口栈变化:每个窗口不再属于单一的栈,而是属于不同的“任务栈”。分屏模式下有两个栈:主栈和副栈。
  • 大小变化:窗口可以动态调整大小,WMS需要重新计算布局。
  • 焦点切换:用户点击哪个窗口,焦点就切换到哪个窗口。但画中画窗口比较特殊——它不能获得焦点。
  • 配置变更:窗口大小变化会触发Configuration变更,Activity需要重新布局。

调试多窗口问题,我推荐用这个命令:

adb shell dumpsys window windows | grep -E "mStackId|mBounds|mHasFocus"

它会输出每个窗口的栈ID、边界信息和焦点状态。配合dumpsys activity activities可以看Activity的栈信息,两者对照着看,问题往往一目了然。

一个典型案例:分屏模式下,App的UI显示不全。原因是App没有适配多窗口,它的布局写死了固定尺寸。解决方案是在AndroidManifest.xml中声明android:resizeableActivity="true",或者在onConfigurationChanged()中动态调整布局。

27.5 窗口管理调试的“三板斧”

我总结了一套窗口管理调试的流程,分享给你:

  1. 看布局:adb shell dumpsys window windows看窗口列表,确认每个窗口的type、layer、bounds、focus状态。
  2. 看焦点:adb shell dumpsys window focused确认焦点窗口是否正确。
  3. 看输入:adb shell dumpsys input看输入事件的分发路径,确认事件是否被正确窗口接收。

这三步走下来,80%的窗口问题都能定位到根因。

27.6 窗口层级与焦点流转示意图

下面这张图展示了典型的窗口层级结构和焦点流转路径。我建议你把它保存下来,调试时对照着看:

窗口层级结构与焦点流转 应用窗口层 (Type: 1-99) Activity主窗口 子窗口层 (Type: 1000-1999) Dialog、PopupWindow 系统窗口层 (Type: 2000-2999) 状态栏、导航栏、输入法 系统错误窗口层 (Type: 2010) ANR对话框、崩溃提示 系统覆盖窗口层 (Type: 2013) 悬浮窗、Toast Z-order 从低到高 焦点流转路径 用户触摸 → WMS命中测试 → 找到最上层可聚焦窗口 → 分配焦点

这张图里,Z-order从下往上递增。焦点流转的路径是:用户触摸屏幕 → WMS做命中测试 → 找到最上层可聚焦的窗口 → 分配焦点。

注意:系统覆盖窗口虽然层级高,但如果设置了FLAG_NOT_FOCUSABLE,焦点会穿透它,落到下面的窗口上。这就是为什么Toast不会抢走焦点。

27.7 实战:一个典型的多窗口Bug排查

我记得有一次,用户反馈在分屏模式下,App的输入框点不了。我按“三板斧”排查:

  • 第一步:dumpsys window windows — 发现App窗口的bounds正常,但有一个系统窗口覆盖在上面。
  • 第二步:dumpsys window focused — 焦点在那个系统窗口上,不在App窗口。
  • 第三步:dumpsys input — 输入事件被系统窗口拦截了。

根因是:分屏模式下,系统窗口的创建逻辑有Bug,它没有设置FLAG_NOT_TOUCHABLE。修复后问题解决。

我的习惯:每次调试窗口问题,我都会先拍一张dumpsys window windows的完整输出,然后对照着代码看。很多时候,问题在输出里就已经很明显了。

窗口管理调试,说白了就是搞清楚“谁在上面、谁有焦点、事件去了哪里”。掌握了这三个问题,你就能解决绝大多数窗口相关的问题。


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