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可以查看当前所有窗口的层级信息。输出里会显示每个窗口的mBaseLayer和mSubLayer,这两个值加起来就是最终的Z-order。
27.3 焦点管理调试
焦点管理是另一个容易让人头疼的问题。焦点决定了输入事件(触摸、按键)该发给哪个窗口。
WMS维护了一个“焦点窗口”的概念。当用户触摸屏幕时,WMS会找到触摸点所在的窗口,然后把事件发过去。但如果窗口没有获得焦点,它可能收不到键盘事件。
我曾经遇到一个Bug:用户按返回键,App没有响应。查了半天,发现是一个透明的系统窗口抢了焦点。透明窗口?对,它设置了FLAG_NOT_TOUCHABLE,但没设置FLAG_NOT_FOCUSABLE。结果就是:它挡住了所有按键事件。
避坑指南:创建透明覆盖窗口时,一定要同时设置FLAG_NOT_TOUCHABLE和FLAG_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 窗口管理调试的“三板斧”
我总结了一套窗口管理调试的流程,分享给你:
- 看布局:用
adb shell dumpsys window windows看窗口列表,确认每个窗口的type、layer、bounds、focus状态。 - 看焦点:用
adb shell dumpsys window focused确认焦点窗口是否正确。 - 看输入:用
adb shell dumpsys input看输入事件的分发路径,确认事件是否被正确窗口接收。
这三步走下来,80%的窗口问题都能定位到根因。
27.6 窗口层级与焦点流转示意图
下面这张图展示了典型的窗口层级结构和焦点流转路径。我建议你把它保存下来,调试时对照着看:
这张图里,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