简介:面向Android系统研发人员的一份WMS深度解析文档,尤其适合工作1~3年、希望理解窗口管理与渲染机制的开发者。内容以WindowManagerService(WMS)启动流程为主线,先梳理Window、Surface、WindowManager、PhoneWindowManager等基础概念,再剖析WMS启动时核心成员变量的初始化,以及窗口令牌(WindowToken)、窗口状态(WindowState)、显示内容(DisplayContent)的注册与维护方式,整体结构由浅入深。针对窗口添加、布局调整和销毁过程,文档结合SampleWindow完整演示了从addView到最终销毁的代码流程,明确了窗口显示次序(z-order)的确定规则和窗口动画管理方式,并覆盖输入系统中转、Surface管理等内容。资源为单个docx文档,约2.82MB,包含清晰的流程图与代码示例,方便对照练习。目前已有157人学习,可作为学习Android系统底层机制的参考资料,建议结合实际项目对关键流程做断点调试与验证,文末还提供JUnit测试用例帮助加深对WMS核心组件的理解。
1. WMS到底在管什么:先搞清楚问题域
做Android Framework开发或者遇到窗口层级错乱、触摸事件诡异、弹窗死活不显示这类问题时,我们总会绕到一个核心服务——WMS,全称WindowManagerService。说直白点,它就是系统里所有窗口的“大管家”。
很多人学了Android几年还在用Activity、View那套上层API,觉得窗口不就是setContentView一下的事情吗?确实,日常业务开发不需要碰WMS,但一旦你开始搞系统定制、做车机/电视这种特殊形态的Android、或者优化启动速度、排查Input事件失灵,WMS就是绕不过去的一座山。
这篇文章我打算按三条线把WMS讲透:它是怎么启动的、内部有哪些关键组件、窗口从添加到显示再到接收触摸的完整链路是什么。另外会穿插一些我自己排查问题时的实操记录和踩坑经验,尤其是dumpsys window这类命令到底该怎么用它帮我们定位问题。适合有一定Android基础、想往系统层深入的同学,也适合正在做系统定制或者遇到诡异窗口问题的开发朋友。
我从第一次在源码里看到WindowManagerService.java这个两千多行的类开始,到现在搞过数次窗口权限适配、上下分屏、悬浮窗兼容,最大的感受是:WMS的东西非常体系化,但如果能先把“它解决什么问题”搞清楚,后面读源码的效率会高很多。
2. WMS的启动流程:从SystemServer到窗口服务就绪
2.1 SystemServer里的启动顺序为什么这么重要
Android系统启动最后一步,是由zygote fork出来的SystemServer进程承载的。WMS不是自己跑的,它是在SystemServer的startOtherServices()方法中被实例化的,这个时间点选得很讲究:必须在AMS(ActivityManagerService)、PowerManagerService、PackageManagerService这些基础服务就绪之后,但在WindowManagerPolicy、InputManagerService配套完成之后,窗口服务才能正式开始工作。
为什么顺序这么重要?因为WMS启动时要做几件事:读取显示设备信息、初始化输入系统、注册系统服务、恢复之前保存的窗口状态。如果AMS还没准备好就启动WMS,那后面Activity要显示窗口时没人配合;如果InputManagerService还没准备好,窗口管理得再漂亮也接收不到触摸事件。
我用一句话概括这个时期的关键代码路径:SystemServer.startBootstrapServices()负责最基础的服务(AMS、PowerManager等),startCoreServices()负责电池、UsageStats等,startOtherServices()才轮到WMS、InputManagerService、StatusBarManager这些。WMS就属于“其他服务”里最大块头的那一个。
2.2 WMS构造过程:不只是new一个对象
看WindowManagerService.main()方法你会发现,它是通过SystemServerMain的反射机制被调用的。真正初始化分三步:
- 第一步,创建
WindowManagerService实例,构造函数里会初始化RootWindowContainer、DisplayManager、InputManagerService等重要成员; - 第二步,调用
WindowManagerService.onInitReady(),这个阶段WMS会拿到ActivityTaskManagerService的引用(注意,现代Android里ATMS和WMS是分开的两个服务),并且通过WindowManagerPolicy初始化系统UI的策略; - 第三步,调用
WindowManagerService.systemReady(),此时会恢复持久化窗口,通知InputMonitor开始监控。
整个过程最容易被忽略的是DisplayManager的接入。WMS并不是自己去枚举物理屏幕,而是通过监听DisplayManagerService的显示变化事件,动态创建对应的DisplayContent对象。多屏环境下,每块屏幕都有独立的窗口层级,这个设计在大屏、车机这类场景非常关键。
实操中我经常用
adb shell dumpsys window displays看当前所有显示设备的信息。排查“窗口显示到别的屏幕上了”这种问题,第一个命令就是它。
另外有一点值得知道:WMS是通过LocalServices.addService(WindowManagerInternal.class, ...)把自己注册为系统内部服务的,但对外则是通过Binder注册为"window"服务。也就是说,应用层的WindowManager最终是通过Binder跨进程调用到WMS的addView、removeView、updateViewLayout这三个核心方法的。
3. 核心组件与数据结构:读懂WMS的“内部器官”
3.1 WindowToken、WindowState、DisplayContent三件套
窗口管理机制说复杂很复杂,说简单其实可以用三个核心类串起来:WindowToken、WindowState、DisplayContent。我把它们类比成“楼盘的预售许可、一间实际的房子、整个楼盘地块”。想在一个屏幕上开窗口,必须先有Token(许可),Token主要对应一个组件类型的宿主,比如一个Activity对应一个ActivityRecord的Token,一个输入法对应一个InputMethodToken。没有Token的窗口会被直接拒绝,这是WMS的第一道安全校验。
WindowState才是真正的窗口“实体”,它保存了一个窗口的所有状态:窗口的LayoutParams、所在Display、可见性、动画状态、Z轴顺序、与Input系统的关联关系等。可以说WindowState就是WMS内部对每一个可见/不可见应用窗口的完整描述。
DisplayContent则代表一块物理屏幕的窗口管理域。它内部有WindowList,按Z轴顺序从底到顶排列这个Display上所有窗口。所有关于层级调整的逻辑,最终都落在这个列表的操作上。
3.2 策略类:PhoneWindowManager与WindowManagerPolicy
WindowManagerPolicy是WMS中容易被忽略但对实际体验影响极大的接口,最常见实现是PhoneWindowManager。它管什么?它管的是“系统窗口和普通应用的窗口怎么共处”,包括:
- 屏幕旋转、锁屏、按键(音量、电源)处理;
- 状态栏、导航栏、壁纸的层级排布;
- 对于HOME键、最近任务这些系统设备的拦截和窗口跳转。
简单理解,WindowManagerPolicy相当于给WMS提供了“哪些窗口应该在上面、哪些按键要特殊处理”的决策依据。比如车机上状态栏不想被全屏应用遮住,你要改的就是这个策略类中状态栏window的层级。
InputMonitor是WMS与输入系统之间的桥梁。它的工作是维护输入窗口列表——每次窗口添加、删除、层级变化时,都会更新一份“当前哪个窗口接收触摸”的映射表,交给InputDispatcher使用。
3.3 核心Binder接口:Session与IWindow
应用进程是不能直接跨进程调用WMS方法的。每个应用进程在第一次添加窗口时,会通过WindowManager.openSession()创建一个Session对象,这个Session是WMS内部一个Binder实体,它表示“来自某个进程的窗口管理会话”。WMS通过Session可以知道调用方是谁,从而做权限判断(比如TYPE_APPLICATION_OVERLAY这类窗口有权限限制)。
同时,每个ViewRootImpl对应一个IWindow接口的Binder代理,它用于WMS向应用进程反向通信,比如通知应用“你的窗口位置变了”、“你的窗口要被移除”。这套双向Binder机制保证了窗口管理的可靠性。
排查问题的时候,
adb shell dumpsys window windows输出的每个窗口块里会带session=...和mWindow=...,前者能看到是哪个进程在管理这个窗口,后者能对应到具体App的ViewRootImpl。
4. 窗口管理机制:从addWindow到显示在屏幕上的完整链路
4.1 应用层怎么把窗口交给WMS
一个Activity要在屏幕上显示,走的是WindowManager.addView(view, params)。这里面的params是WindowManager.LayoutParams,包含了窗口类型(type)、宽高、位置、Flags、软键盘模式等关键信息。这个调用最终通过Session跨进程到WMS的addWindow()方法。
上层的ViewRootImpl在这个流程里做了大量工作:它创建了InputChannel、Surface,负责与WMS的布局通信,也是“一个窗口对应的视图层级管理器”。举一个例子,requestLayout()后ViewRootImpl会调用relayout(),这个方法的返回值RelayoutResult包含了窗口的最终尺寸、位置、可见性,以及SurfaceControl(用来真正往屏幕上画图)。不少新手搞不清View和WMS的关系,这里再强调一句:View负责画什么,WMS负责决定窗口在哪、多大、能不能被看到、触摸该给谁。
addWindow内部会走一系列校验:有没有Token、类型是否合法、进程是否有对应权限,然后创建WindowState,并把它插入到DisplayContent的窗口列表中,最后触发performLayoutAndPlaceSurfacesLocked()。
4.2 Layout与Z轴:窗口顺序是怎么定的
Windows的Z轴顺序真是很多问题的根源。WMS中Z轴的排序由几个维度共同决定:mBaseLayer(根据窗口类型映射的基础层级)、mSubLayer(从属窗口依附于主窗口的偏移)、以及mAttrs.type的实际大小。
举个例子:状态栏type是TYPE_STATUS_BAR,它对应一个比较高层级;普通应用窗口type是TYPE_APPLICATION,层级相对低;而系统弹窗TYPE_APPLICATION_OVERLAY介于两者之间。同一类型下,后添加的窗口在上方,z-order最终通过WindowState的mWinAnimator交给SurfaceFlinger合成。
如果你做过悬浮窗,一定遇到过“我的View明明调用了bringToFront怎么还是被盖住”的问题。因为WMS管理的是窗口层级,不是View层级。你需要通过WindowManager.updateViewLayout或者重新addView改变窗口类型,或者直接修改LayoutParams中的层级属性。
控制层级的一个常见误区是直接用
params.type = TYPE_APPLICATION_OVERLAY,这个类型在Android 10以上是要求权限的,普通App如果targetSdk比较高,不申请SYSTEM_ALERT_WINDOW会被直接拒绝。系统应用如果只想比普通应用高、比状态栏低,应该查询系统里现有的层级再做微调,而不是盲目用大类型。
4.3 窗口不可见、动画与Surface分配
窗口要真正显示出来,光有窗口状态还不够,还得有Surface去承载绘图内容。WMS负责从SurfaceControl创建并管理每个窗口对应的Surface。当窗口被添加、尺寸变化、或者需要动画后,WMS会调用WindowStateAnimator处理窗口动画框架,然后把几何变化交给SurfaceFlinger。
这解释了为什么我们在ANR日志或dumpsys window里经常看到Surface相关的信息——窗口可见性、Surface的缓冲区大小、帧率等都能从WMS的状态里追踪到。我排查过一个“黑屏但Activity生命周期正常”的问题,最后用dumpsys SurfaceFlinger --list看layer状态,发现是Surface被WMS给hide了,而hide的原因又是窗口动画一直没结束。这种问题不抓WMS状态几乎无法定位。
4.4 触摸事件:WMS不是直接发事件的
很多人会误以为“WMS负责触摸分发”,其实不对。真正分发触摸事件的是InputDispatcher,它属于InputManagerService。WMS在这里的角色是告诉InputDispatcher“现在屏幕上有哪些窗口,它们的点击区域分别是多少”,具体通过InputMonitor来维护。
每次DisplayContent的窗口层级变化、窗口位置尺寸变化,WMS都会调用updateInputWindows方法来更新InputDispatcher的窗口信息。一旦触摸事件发生,InputDispatcher会遍历“可接收触摸的窗口列表”,找到最上层的目标窗口,然后把事件写入对应窗口的InputChannel里。
所以“点了按钮没反应”的排查路径很有讲究:先去dumpsys input看事件派发到了哪个窗口,再看那个窗口是不是因为被某个透明的全屏窗口盖住了。很多时候根本不是你的Button逻辑问题,是窗口层级被盖住或者窗口不可点击了。
5. 实战排查:那些年我们踩过的WMS坑
5.1 has leaked window:常见的窗口泄漏
Android开发中非常经典的一种崩溃是WindowManager.BadTokenException,日志里常写着has leaked window ...。我的记忆里,第一次处理这类问题是在弹窗Dialog时没等Dialog dismiss就关闭了Activity,然后系统拿着已经失效的Token继续调addView。本质就是:你的Token对应的Activity或Window已经不存在了,WMS不认。
解决这类问题的方法我总结了一下:
- 不要在
onStop/onDestroy之后还发异步消息去弹窗,如果确实要弹,先判断isFinishing()和isDestroyed(); - 使用
Dialog时尽量用它自带的dismiss回调做清理,不要另外起线程去关闭; - 把
WindowManager.LayoutParams里的token设置为null在某些场景会绕过Activity的校验,但可能导致浮动窗口无法接收某些输入焦点事件,需要谨慎。
5.2 触摸失灵:先查窗口层级再查布局
有一次做车载系统,反馈是“地图界面左边区域点了没反应”。第一反应可能觉得是View的问题,但后来我用adb shell dumpsys window windows | grep -A 20 mCurrentFocus一看,发现焦点窗口之上还盖着另一个不透明的系统窗口,恰好遮挡了地图左半部分,而且它自己是FLAG_NOT_TOUCHABLE,所以事件被它“吃掉”了却又没有任何视觉展示。
排查触摸问题,按这个顺序来基本高效:
- 第一,
dumpsys input查看TouchStates,看事件最终发给了哪个窗口; - 第二,
dumpsys window windows查看当前Display的窗口列表,注意Z轴最上面的几个窗口是谁; - 第三,检查被点击区域是否被某个高优先级窗口覆盖,比如输入法窗口的“临时区域”、导航栏的触摸区域扩展。
5.3 窗口正常却不显示:从Surface与可见性入手
如果dumpsys window windows里能看到目标窗口的mViewVisibility = 0x0,而且mHasSurface = true,但屏幕上依然看不到,那么方向应该转向SurfaceFlinger图层排查:dumpsys SurfaceFlinger检查这个窗口对应的layer位置、尺寸、alpha是否正常。有一种常见的情况是窗口Layer位置被改到了屏幕外,或者宽高为0。
WMS本身不画任何内容,它只负责窗口的“元数据”管理。真正决定像素展示的是SurfaceFlinger。所以窗口问题排查我建议心里永远有一根链条:应用View层级 → WMS的WindowState → SurfaceFlinger的Layer → 最终硬件合成出图像。链条上任何一环出问题都会看不见,要快速定位就得逐层验证。
关于
TYPE_APPLICATION_OVERLAY还有一点经验:这类窗口的层级高于普通Activity,但又低于状态栏、输入法。弹窗类App如果要显示在输入法上方,可能需要用setInsetProvider或直接在全屏模式下处理软键盘避让,简单调type很难达到预期效果。
5.4 窗口过多导致卡顿
手机上连续打开多个页面后,dumpsys window windows能看到大量已经不可见但仍然存在的WindowState。虽然现代Android对不可见窗口做了relayout时释放Surface的优化,但窗口对象以及它的状态仍是WMS内存的一部分。我以前在一个低端机上压测,打开50个Activity后系统窗口列表已经非常长了,每次performLayoutAndPlaceSurfacesLocked遍历整个列表耗时明显上升。
从架构上避免这类问题的方法就是:及时销毁不再需要的窗口。普通应用用Activity.finish()是常识了,但如果你做了多窗口/自由窗口定制,要特别注意关闭窗口时一定要走完removeView的流程,不要只隐藏Surface。
6. 尾注:学习WMS源码的几个建议
聊了这么多,最后分享一点我反复看源码之后的体会。WMS代码量很大,但真正核心的入口就几个:addWindow、removeWindow、relayoutWindow、performLayoutAndPlaceSurfacesLocked。建议第一次接触的同学不要从头读到尾,而是围绕一条场景去读,比如“我启动一个Activity,WMS到底对我的Window做了哪些操作”,顺着这个场景把上面提到的类串联起来。
第二点建议是,动笔之前一定先学会看dumpsys。你可以在模拟器或真机上故意叠几个窗口,然后对比dumpsys window windows输出变化,这比只看源码印象深得多。dumpsys window的很多字段命名跟源码成员基本一致,比如mHasSurface、mViewVisibility、mCurrentFocus,看懂了这些输出,返回去读源码会顺畅很多。
最后想说的是,WMS虽然底层机制复杂,但它的设计目标其实很简单:在正确的时间,把正确的窗口,放到正确的层级,让正确的人看到,让正确的手指点到。搞懂WMS不一定能让你的日常业务开发突飞猛进,但绝对能让你面对诡异问题的时候不再两眼一抹黑。
本文还有配套的精品资源,点击获取