Appearance
07 WindowManagerService:窗口的入场券与 Z 轴秩序
本页导读:应用画不画得出像素,第一道闸门不是 SurfaceFlinger,而是 WindowManagerService(WMS):窗口类型允不允许你出现、叠在谁上面、拿哪块 Surface、谁能吃掉焦点与 insets,全是它说了算。本页拆:窗口类型与分层、relayout-finish 同步协议、与 SurfaceFlinger 的分工、输入窗注册、insets 体系、动画与 Transition 框架。基线 Android 16(API 36)。
一、窗口即对象:WindowState 与三剑客
- Session:一个应用进程在 WMS 的代理户头,持有该进程全部窗口的 binder 通道(
IWindowSession); - WindowState:一个窗口的服务端档案:属性(
WindowManager.LayoutParams)、帧、可见性、动画状态、对应的输入窗; - WindowToken:窗口的「身份证」。应用窗口的 token 就是
ActivityRecord(第 06 页容器树里一一对应),子窗口挂 parent 窗,系统窗用独立 token。
服务端还握着一张反向通道 IWindow:resize、insets 变化、焦点通知都从 WMS 打回 ViewRootImpl(frameworks/base/core/java/android/view/ViewRootImpl.java 的 resized()/insetsChanged() 家族)。窗口世界是双向呼叫:应用用 Session 上行,WMS 用 IWindow 下行,两条 Binder 方向相反、语义互锁。
java
// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
// 关键入口(示意清单,签名以 AOSP 为准):
// openSession() 应用 attach 窗口世界的入口
// relayoutWindow() 尺寸/可见性协商,返回 Surface 与布局结果
// addToDisplayNoWindowLocked / assignLayer Z 轴排位
// finishDrawingWindowLocked 首帧/变更完成回执二、类型与分层:Z 轴的宪法
WindowManager.LayoutParams.type 决定窗口落在哪个宪法管辖区(常量见 frameworks/base/core/java/android/view/WindowManager.java):
| 区间 | 名称 | 代表 | 特性 |
|---|---|---|---|
| 1~99 | 应用窗 | Activity 窗口 | 归属 Task,参与返回栈 |
| 1000~1999 | 子窗 | 面板/子对话框 | 跟随父窗移动 |
| 2000~2999 | 系统窗 | 状态栏(2000)、输入法(2011)、Toast(2005)、音量面板(2010) | 需权限,层级高于应用 |
| 3000~3999 | 应用启动窗 | starting window | 生命周期极短 |
Z 轴次序由 WindowManagerPolicy(接口在 frameworks/base/core/java/android/view/WindowManagerPolicy.java? 实际为 frameworks/base/services/core/java/com/android/server/policy,以 AOSP 为准)的 getWindowLayer 家族计算:先按类型大区间,再按同类型内 mLayer 序号。Toast/悬浮窗类需要 SYSTEM_ALERT_WINDOW 类权限,Android 6+ 用户可授可撤。
权限侧速记:
- 应用窗无需特权,但受「后台不能加窗」限制(Android 10+ 对后台起系统窗同样收紧,日志
Background window add类字样,以版本为准); - 悬浮窗权限(
SYSTEM_ALERT_WINDOW)Android 6 起进运行时授权;Android 12 起对「精确 ALPHA 动画」「启动画面劫持」类滥用有额外限制(SWE/延迟欺骗防御,以 AOSP 为准); - 音量面板(2010)、截屏预览(2030)等属于「系统自留地」,应用不可申用。
同类型内的相对次序由 mLayer/mSubLayer 计算;dumpsys 输出里的 mLayer 字段就是最终 Z 值的中间量,两个窗口「谁盖谁」之争,看它最诚实。
三、relayout-finish:一次「量体裁衣」的握手
窗口从创建到上屏的核心同步协议:
text
ViewRootImpl.scheduleTraversals → doTraversal
→ relayout:IWindowSession.relayout(WindowState 属性, WindowLayout 出参) [Binder→WMS]
→ WMS:布局计算(mWindowPlacer/WindowLayout)
→ 必要时新建/调整 SurfaceControl(BLAST 时代:给应用一个 BLASTBufferQueue)
→ 返回:帧、可见性位、新的 SurfaceControl、同步事务句柄
→ 应用侧 measure/layout/draw(RenderThread 录制显示列表)
→ queueBuffer → BLAST 提交(第 10 页)
→ finishDrawing:IWindowSession.finish(类型) [Binder→WMS]
→ WMS 确认尺寸一致(防「画完变卦」),把窗从 "waiting for draw" 摘下
→ 通知 ATMS 该 Activity 可标记 resuming/idle(第 06 页)要点:
relayout 决定你能画多大,finish 确认你真画了——旋转/resize 时的「黑边、拉伸帧」多半是这两步时序被应用作死(onMeasure 耗时/不同步);
Android 11 起 SurfaceControl 直接传句柄,Surface 由 BLASTBufferQueue 包装(旧的
Surface+IGraphicBufferProducer通道退役,第 10 页);relayout返回值是一组RELAYOUT_RES_*位(是否被 resize、是否有新 Surface、insets 是否变化等,常量以 AOSPWindowManagerGlobal.java为准),ViewRootImpl 据此决定要不要重走完整遍历——「明明改了尺寸没重画」类玄学,先看这几个位。WMS 侧用「同步组」(
SurfaceSyncGroup,frameworks/base/services/core/java/com/android/server/wm/SurfaceSyncGroup.java,AOSP 目录以实际为准)把多窗变更攒成一次原子事务提交给 SurfaceFlinger——这是「动画中窗口切换不撕裂」的机制根基。
四、与 SurfaceFlinger 的分工:指挥与演奏
- WMS 管逻辑:谁能出现、多大、在谁上面、动画状态机;
- SurfaceFlinger 管物理:Buffer 队列、VSYNC、合成(第 10 页);
- 交接口是
SurfaceControl事务:WMS 把「创建图层/设 z/设 alpha/设 crop/设矩阵」写进Transaction,apply()走 binder 给 SF; - WMS 不碰像素:截图(
captureDisplay/captureLayers)请求也是 WMS 下指令、SF 执行,screencap走的正是这条路。 - 显示配置(刷新率候选、亮度)归 DisplayManagerService 与显示 HAL(第 10/11 页),WMS 只消费「可用显示区域」这一结论。
五、输入窗注册:把「谁有焦点」交给输入系统
WMS 每次布局收敛后,把可见窗口清单打包成 InputWindowHandle 列表,经 InputMonitor/InputManagerService 下发 native InputDispatcher(第 09 页的 setInputWindows);焦点变化则调 setInputFocusWindow。链上三个细节:
- 一个窗口能否吃输入,由
FLAG_NOT_TOUCHABLE/FLAG_NOT_FOCUSABLE等 flag 与可见性共同决定; pauseDispatching/resumeDispatching用于「应用正忙,先别喂事件」的窗口级暂停;- 焦点窗的 token 必须是已 attach 的有效窗,ANR 报告里的
Waiting because it is unresponsive与这里状态直接相关; - 手势导航时代还有「窥视窗」(spy window,Android 12+):SystemUI 的边缘手势层以窥视方式接收全部事件流而不截胡,第 09 页细讲。
六、insets 体系:屏幕边界的和平条约
系统栏、刘海、手势条占据的「边界领土」用 insets 协商。Android 11(API 30)完成体系重写:
- 服务端:
InsetsStateController(services/core/java/com/android/server/wm/InsetsStateController.java)维护每个显示区的 insets source(状态栏、导航栏、IME、刘海); - 客户端:
android.view.InsetsController+WindowInsetsController负责响应:谁消费、谁避让、谁穿透; - 类型:
Type.systemBars/systemGestures/mandatorySystemGestures/ime/displayCutout等组合;应用可用setDecorFitsSystemWindows(false)进入边到边,Android 15+ 默认化后这是必修课(第 00 页版本注记); - IME 是最复杂的 source:弹出/收起动画与内容 resize 用
ime类型 + 同步事务保持同步(三、同步组再次出场)。
实战速查:
| 需求 | 正解 | 常见错解 |
|---|---|---|
| 内容避让系统栏 | setDecorFitsSystemWindows(true)(默认语义) | 手算状态栏高度 |
| 内容画到栏后面 | 关 decor fit + 自行处理 insets | 直接隐藏系统栏 |
| 跟随 IME 动画 | insets 动画 + WindowInsetsAnimation 回调 | 监听键盘可见性轮询 |
| 全屏沉浸 | hide(systemBars) + 行为配置 | 废弃的 SYSTEM_UI_FLAG 大杂烩 |
dumpsys window 与 dumpsys window displays 里 InsetsState 段就是谈判底牌。
七、动画与 Transition:从状态机到框架
- 旧时代:
WindowStateAnimator+AppTransition/TransitionAnimation:系统代放「窗口级」动画(缩放、淡入),应用拿不到控制权,自定义转场靠 overridePendingTransition 缝缝补补; - 新时代(Shell Transitions):
TransitionController(services/core/java/com/android/server/wm/TransitionController.java)把一次可见性变化打包成Transition对象:收集参与窗(leash 化的容器)→ 发给 SystemUI 侧TransitionHandler链执行 →finishTransition原子提交(第 10 页 SyncFramework 同一思想);Android 12 起默认启用,旧路径 Android 14 前后移除(以 AOSP 为准); - 应用侧接口:Activity 转场动画
overridePendingTransition(旧)、ActivityOptions、ActivityTransition(API 34 的 enter/exit 自定义);预测式返回手势(API 34/35,OnBackInvokedDispatcher)同样吃 transition 的 leash; - 动画落位靠
SurfaceAnimator:把容器 reparent 到 leash 层做变换,结束后回嫁——理解「动画中 dumpsys window 看到临时层级」就不慌了。
八、实例:软键盘弹出的一圈系统
把一次键盘弹出走一遍,能同时检验本页所有零件:
- 输入框获焦 → 应用
WindowInsetsController发出show(ime()); - 请求到
WindowManagerService/IME 子系统:Imms找到当前 IME 服务(wm/InputMethodManagerService.java),拉起 IME 进程(第 06 页孵化链,IME 也是普通 App); - IME 窗口
relayout入场(第三节握手),类型 2011 高于应用窗,adjustResize/adjustPan语义由 WMS 换算成应用窗的可用帧; InsetsStateController把 IME source 的高度变化广播给全部相关窗口;- 应用窗收到新 insets,重布局;键盘自身内容走 BLAST 提交(第 10 页);
- 整个「升起动画」被 SurfaceSyncGroup 打包成原子事务——键盘、被顶起的内容、导航栏色不会各走各的节奏;
- 收起同理;若键盘进程猝死,
DeathRecipient(第 04 页)清理输入焦点,焦点窗收回。
顺带记住常用窗口 flag 的省电联动:FLAG_KEEP_SCREEN_ON 由 WMS 转告 PowerManagerService 维持屏幕(第 11 页 wakelock 家族之外的「借光」机制)。
九、实战:dumpsys window 的正确打开方式
dumpsys window 的默认输出非常厚,实战永远带子命令与 grep;下面四条命令覆盖 80% 的窗口问题。
bash
adb shell dumpsys window displays # 显示/区域/insets 快照
adb shell dumpsys window windows | grep -e "Window #" -e "mViewVisibility" # 表格内书写需转义
adb shell dumpsys window visible # 当前真可见的窗(SF 侧用 dumpsys SurfaceFlinger 对照)
adb shell dumpsys window anim # 动画/transition 状态(子命令名随版本有出入)典型判例:画面没出来——先 visible 里有没有窗;有窗不可见——看 mViewVisibility/alpha/isReadyForDisplay;有窗可见但黑——SF 图层段对照第 10 页。
- 命令输出随版本漂移是常态:先
dumpsys window -a里找段落名,再定位字段,不要背行号。
再补两条日常:
mAppVisible=false而界面明明在——多为 starting window 挡刀或同步组未 finish;dumpsys window transitions(子命令以版本为准)看 transition 是否卡住不收,动画类「画面定格」问题多出于此。
十、常见窗口问题清单
| 症状 | 第一嫌疑 | 取证 |
|---|---|---|
| 加窗被拒 | 类型无权限/后台加窗限制 | logcat add window 权限拒绝栈 |
| 悬浮窗盖不住输入法 | 2005/2038 类低于 2011,层级宪法如此 | dumpsys window 对照类型 |
| 旋转后内容拉伸 | relayout-finish 间应用未重画 | relayoutResult 位 + 应用帧日志 |
| 动画撕裂/黑块 | 同步组漏提交、第三方动画未入组 | dumpsys window anim + perfetto |
| 刘海内容被切 | displayCutout 模式未处理 | InsetsState 里 cutout source |
| 焦点丢失收不到键 | 焦点窗不可见/token 失效/暂停分发 | dumpsys input 焦点段(第 09 页) |
十一、版本注记
- Android 8~9(API 26~28):画中画、多窗口初体验;层级体系沿用大区间宪法。
- Android 10(API 29):Shell Transition 机制落地(默认未全量)。
- Android 11(API 30):WindowInsets API 重写;SurfaceControl 通道替换旧 Surface 通道。
- Android 12(API 31):BLAST 全面默认;Shell Transitions 默认启用;
TaskFragment容器(第 06 页)。 - Android 13~14(API 33~34):预测式返回;大屏/桌面化(窗口化多任务)大量 WMS 适配;
RemoteTransition类机制让自定义转场进入受控开放期(以 AOSP 当前 API 级为准)。 - Android 15~16(API 35~36):边到边默认、letterbox 策略收紧;Transition 体系继续吸收旧动画残余(以 AOSP 为准)。
- 兼容性提醒:WMS 的内部类从不在 SDK 稳定承诺内,反射改窗口行为的「黑魔法」在 Android 15+ 被非 SDK 名单与 CompatChanges 持续绞杀,别把赌注押在这。
- Android 16 上
WindowManagerService仍是单类巨无霸,但InsetsStateController、TransitionController、TaskFragment等子域持续模块化,读码按子域切入。
本页小结
本页小结:学 WMS 的最佳路径是跟着一个窗口的生命周期读源码:add → relayout → finish → 动画 → remove;每一站对应本页一节。WMS 是「逻辑世界」的窗口政府:类型宪法决定能不能出现,relayout-finish 握手确定画多大画完没有,输入窗注册交出焦点外交权,insets 条约划定边界领土,Transition 把动画从私刑升级成程序正义。SurfaceFlinger 只认事务不认人——所有故事最后都要收敛在一句
Transaction.apply()里。
参考
- 窗口与 insets 官方说明:developer.android.com/guide/ui (WindowManager/WindowInsets 章) 与 developer.android.com/develop/ui/navigation/insets(Insets 指南)
- WindowManagerService 源码:cs.android.com/android/platform/frameworks/base/+/main/services/core/java/com/android/server/wm/WindowManagerService.java
- TransitionController 源码:cs.android.com/android/platform/frameworks/base/+/main/services/core/java/com/android/server/wm/TransitionController.java
- 图形栈衔接:source.android.com/docs/core/graphics(SurfaceFlinger 章)
- 沉浸式/全屏官方指南:developer.android.com/training/articles/immersive-mode