☰
Android窗口打开全流程:addView到SurfaceFlinger上屏
2026/10/2 3:29:39 网站建设 项目流程

说实话,窗口这套东西在 Android 里属于“平时用不到、出事就头大”的典型。应用开发只知道调WindowManager.addView,系统开发天天对着WindowManagerService(也就是俗称的 WMS)的源码发愁。我前一阵排查一个“窗口偶尔白屏、打开慢半拍”的问题,把从应用进程到系统进程、再到 SurfaceFlinger 的整条链路重新读了一遍,发现很多代码流程如果不画出来,光靠看源码真的很绕。这篇文章就把窗口打开全流程的代码路径拆开讲清楚,从addView开始,到 WMS 登记窗口、分配 Surface、计算布局,最后到 SurfaceFlinger 合成上屏,跟着代码走一遍。我这里统一以 Android 11/12 附近的源码为准,版本差异比较大的地方我会单独说明。

1. 从一次 addView 请求开始说起:窗口到底是怎么打开的

1.1 真正发起 addView 的地方不在 WindowManager 本身

大多数开发者的认知是:我调了WindowManager.addView(view, params),窗口就创建了。这话对也不对。WindowManager只是个接口,真正的实现是WindowManagerImpl,而它内部又会把请求转给进程级的单例WindowManagerGlobal。WindowManagerGlobal.addView做的第一件事不是通知系统,而是先给这个 View 配一个<span >ViewRootImpl</span>。

// WindowManagerGlobal.addView 的简化逻辑 public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) { // 线程检查:必须在主线程 if (mLooper != Looper.getMainLooper()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } ViewRootImpl root; synchronized (mLock) { root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); root.setView(view, wparams, panelParentView); } }

所以一次addView调用,真正做的是“在应用进程里建立一棵视图树和它的控制器”。ViewRootImpl是整个窗口消息驱动的心脏,你说的窗口测量、布局、绘制、和系统交互,全在里面。

这里有个容易误解的点:WindowManagerGlobal是进程级单例,也就是说一个应用进程里所有窗口都共用这一个对象。系统进程里对应的 Session 也是复用的,后面讲 Binder 通道时会看到这个设计的影响。

1.2 ViewRootImpl 建立与线程约束

new ViewRootImpl(context, display)这个构造函数里,最重要的工作是拿到与 WMS 通信的IWindowSession。代码里是这么调的:

mWindowSession = WindowManagerGlobal.getWindowSession();

这个getWindowSession()内部会通过WindowManagerService.openSession()拿一个 Binder 代理。第一次调用时建立连接,之后整个进程所有窗口都复用同一个 Session 对象。Session 在系统进程那一端保存了调用者的pid、uid、mProcessName等信息,所以 WMS 做权限判断时,能知道“是哪个进程在请求窗口”。

线程约束也必须在这里提。窗口操作绝大多数要求主线程,原因不是 Binder 调用本身需要,而是ViewRootImpl里的 View 树不是线程安全的,它的scheduleTraversals、performTraversals都依赖 Choreographer 的帧回调,这套机制只允许主线程驱动。你在子线程里直接addView,基本都会在WindowManagerGlobal.addView的线程检查处被拦下来。

1.3 setView 中的跨进程调用与 IWindow

ViewRootImpl.setView是真正开始跟系统交互的地方。它的流程可以压缩成三步:

  1. 把 View 挂到ViewRootImpl的 View 层级里;
  2. 创建ViewRootImpl.W这个IWindow.StubBinder 对象,它代表“应用进程里的这个窗口”的远端接口;
  3. 通过mWindowSession.addToDisplay把这个窗口注册到 WMS。

关键调用是这样的:

// ViewRootImpl.setView 中(伪代码,省略了大量字段) mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mView.getDisplay().getDisplayId(), mTmpFrame, mTmpContentInsets, mTmpStableInsets, mTmpOutsets, mTmpDisplayedContentInfo, mInputChannel);

mWindow是IWindow类型的 Binder 对象,WMS 后续要通知应用“你的窗口位置变了”“你的窗口可见性变了”时,就通过它回调。这也是个容易忽略的地方:从这套设计一开始,应用和系统就不是简单的“一问一答”,而是双向 Binder 通信。addToDisplay是一次同步调用,应用进程会阻塞等 WMS 返回窗口的初始位置信息。如果 WMS 那边锁竞争严重,这个同步调用就会直接变成“应用主线程卡顿”的元凶之一。

2. Session.addToDisplay 与 WMS.addWindow 的校验逻辑

2.1 IWindowSession 这条 Binder 通道传了什么

IWindowSession是定义在android.view包下的 AIDL 接口,实现在系统进程的Session类里。它管的不只是添加窗口,relayout、addWindow、remove、setInsets这些窗口生命周期操作都走它。

一次addToDisplay调用,传递的数据可以分成几类:

数据作用
IWindow client系统进程回调应用侧窗口事件的 Binder
LayoutParams attrs窗口类型 type、token、flags、坐标、软键盘模式等
displayId目标显示设备
viewVisibility当前 View 的可见状态
一系列out参数WMS 计算后返回的窗口 Frame、Insets 区域
InputChannel输入事件通道,首次添加时建立

注意这里传的LayoutParams是一次 Parcel 拷贝。你在应用侧设置 token、type、flag,到 WMS 那边就已经是另一个对象了。很多“为什么改了 params 没生效”的排查,最后都要回到这里确认数据有没有真的传过去。

2.2 addWindow 的三段校验

Session.addToDisplay收到应用请求后,会转到WindowManagerService.addWindow。这个名字听起来像是“添加一个窗口”,实际含义是“在 WMS 的窗口表里登记一个窗口状态对象”。真正的 View 和绘制内容根本不在这里。

addWindow开头的校验分三块:

  1. token 校验:type为应用窗口时,LayoutParams.token必须是有效的IApplicationToken;type是子窗口(1000-1999)时,必须有合法的父窗口。这里最容易挂的就是BadTokenException。
  2. type 校验:窗口 type 必须落在应用窗口(1-99)、子窗口(1000-1999)、系统窗口(2000+)的合理区间内。
  3. 权限校验:一些特殊 type 需要权限,比如TYPE_APPLICATION_OVERLAY需要SYSTEM_ALERT_WINDOW,注入事件窗口需要INJECT_EVENTS权限。
校验失败典型原因对应异常
token 无效Activity 已销毁,但还拿 token 去加子窗口BadTokenException
父窗口不存在子窗口先于父窗口 addViewBadTokenException
权限不足未声明SYSTEM_ALERT_WINDOW就弹悬浮窗SecurityException
Display 不存在displayId 无效或已移除IllegalArgumentException

还有一个细节:addWindow的返回值是一堆ADD_*错误码,比如ADD_OKAY、ADD_BAD_APP_TOKEN、ADD_PERMISSION_DENIED。应用侧ViewRootImpl拿到非ADD_OKAY后,才会转换成各种异常往外抛。所以你在应用层看到的BadTokenException,其实是系统进程跨进程返回错误码之后,应用进程自己抛的。

2.3 服务端户口:WindowState 与窗口层级

校验通过后,WMS 会创建WindowState对象,这是系统进程里最重要的窗口数据结构。WindowState持有:

  • 对应的IWindowBinder 代理;
  • LayoutParams的副本;
  • 所属的WindowToken;
  • 所在DisplayContent;
  • 和 Surface 强相关的SurfaceControl。

WindowState创建后会放进mWindowMap,同时加到所属DisplayContent的窗口列表里。到了这一步,“窗口”在系统侧才真正有了户口。

老版本源码里还有个WindowStateAnimator,负责和 Surface 动画有关的状态管理。Android 12 前后这部分被合并进了WindowState,职责是一样的。如果你看的是老代码,看到这个类不用慌,它跟“动画”关系不大,更多是管理 Surface 生命周期。

同一时刻的窗口层级也在这里计算。应用窗口的基础层级由窗口 type 决定,同一个 Activity 里的子窗口再根据WindowManager.LayoutParams里的x、y、gravity等算出 sub layer。层级计算放在后面讲 SurfaceFlinger 时会继续展开。

3. relayoutWindow:布局参数回流与 Surface 的第一次亮相

3.1 为什么要两次往返

addToDisplay只是登记户口,窗口有多大、位置在哪、Surface 是什么,都没确定。应用的 View 树要测量布局,必须知道窗口最终会占据多大区域。这一步就是relayout的职责。

ViewRootImpl.setView执行完addToDisplay后会主动requestLayout(),随即scheduleTraversals()触发一次完整的 View 遍历。在performTraversals方法里,代码会先判断是否需要跟 WMS 做relayout,第一次遍历时必然要。这就是为什么窗口打开会有两次典型的 Binder 往返:

  1. addToDisplay:注册窗口,返回初始 Frame;
  2. relayout:根据应用请求宽高和系统策略,计算最终布局结果,并返回 Surface。

这两次调用缺一不可。addToDisplay相当于“我要开窗”,relayout相当于“窗开多大、窗玻璃在哪”。

3.2 SurfaceControl 的创建与 Surface 的绑定

relayout在 WMS 端对应relayoutWindow。这里最核心的事情是:如果这个窗口还没有 Surface,就创建SurfaceControl。

// WMS.relayoutWindow 中关于 Surface 的部分(示意,非完整源码) if (win.getSurfaceControl() == null) { win.createSurfaceControl(); // 内部等价于: // mSurfaceControl = new SurfaceControl.Builder(mSurfaceControlFactory) // .setName(win.getName()) // .setFlags(...) // .build(); }

SurfaceControl是系统进程持有的、对应一个显示层的句柄。应用侧拿到的是Surface,它和SurfaceControl共享同一个底层BufferQueue的生产者端。这个Surface通过relayoutWindow的outSurface参数传递回应用进程。

有个细节要注意:relayout不是每次都重建 Surface。只有第一次 relayout 时窗口还没有 Surface,才会走创建流程。后面如果窗口尺寸、位置变化,通常只是修改SurfaceControl的属性,而不是重建整个 Surface。真正触发 Surface 重建的场景很有限,大多是 Surface 相关崩溃或 GPU 上下文重置。

3.3 performTraversals 的测量布局绘制节奏

ViewRootImpl.performTraversals是 View 遍历的总调度,顺序很讲究:

  1. 先 relayout,拿到frame和 Insets;
  2. 再performMeasure,用窗口真实尺寸约束测量;
  3. 然后performLayout;
  4. 最后performDraw。

其中第 1 步和第 2 步之间有个强依赖:没有frame就没法确定 MeasureSpec。这也是首次窗口打开无法跳步的原因。

绘制阶段,现代的 Android 基本都是硬件加速。performDraw内部会走HardwareRenderer,它把 View 的DisplayList转换成 GPU 指令,再通过Surface提交到BufferQueue。此时 buffer 还没上屏,只是提交给了系统合成端。很多开发者看到“draw 完成”就以为显示出来了,其实中间还隔着 SurfaceFlinger 这一层,这就是后面章节要讲的。

首次窗口打开时,relayout 返回 Surface 后,View 树才真正开始测量布局,所以状态栏、桌面那些动画能看到一个“窗口从空白到有内容”的过渡。如果 Surface 创建成功但应用迟迟不画第一帧,那就是白屏。

4. 从 WMS 到 SurfaceFlinger:窗口几何信息如何上屏

4.1 Transaction 提交位置和层级

WMS 本身不画任何像素,它只负责两件事:算窗口的几何属性,然后把几何属性通过SurfaceControl.Transaction提交给 SurfaceFlinger。这个“几何属性”包括层位置、层大小、裁剪区域、层级顺序、透明度等。

// WMS 计算完成后给窗口层提交几何信息(示意) SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.setLayer(surfaceControl, layer); t.setPosition(surfaceControl, frame.left, frame.top); t.setWindowCrop(surfaceControl, frame.width(), frame.height()); t.setAlpha(surfaceControl, alpha); t.apply();

Transaction是一次原子提交。WMS 不会把WindowState的每个字段都直接同步给 SF,而是把“这一帧窗口应该长什么样”打包成一个事务。SF 拿到事务后统一更新合成状态,避免多个窗口状态不一致导致闪烁。

窗口层级计算通常在assignWindowLayers里完成,各类型窗口的 base layer 按 type 区分,同类型窗口内部再按 WindowState 的mSubLayer排序。层级的最终表现就是 Z 轴顺序:谁盖在谁上面。

4.2 应用侧绘制与 BufferQueue 的关系

应用进程和 SurfaceFlinger 之间,真正的数据通道是BufferQueue。应用侧Surface是生产者,SF 侧Layer里包含消费者。应用每画完一帧,就把 buffer 通过queueBuffer交给消费者。

这里的 vSync 驱动链路是:

  1. Choreographer收到 vsync 信号;
  2. 回调ViewRootImpl.TraversalRunnable;
  3. 执行performTraversals和performDraw;
  4. HardwareRenderer渲染完成后syncAndDrawFrame;
  5. Surface提交 buffer 到 BufferQueue;
  6. SurfaceFlinger 在下一次合成时读取 buffer。

注意:WMS 和 SF 的沟通是事务,应用和 BufferQueue 的沟通是 buffer。两套机制相互独立又耦合在一起:buffer 里是内容,transaction 里是内容如何呈现。窗口打开慢,经常就是这两套机制没对齐。

4.3 “窗口打开完成”到底指什么

从代码流程看,“窗口打开完成”至少有两个层面的标准:

  • WMS 层面:WindowState已经注册,Surface 已创建,状态走到READY_TO_SHOW或HAS_DRAWN;
  • 用户体验层面:第一帧真正被 SurfaceFlinger 合成并输出到屏幕。

这两个标准之间可能隔着一帧甚至多帧。系统判断窗口可以显示,通常要等应用的第一帧 buffer 提交到 BufferQueue 后才能确定。WindowState里mDrawState字段就是干这个的,状态机大致是:

mDrawState含义
NO_SURFACE窗口还没创建 Surface
DRAW_PENDINGSurface 已创建,等待应用提交第一帧
COMMIT_DRAW_PENDING第一帧已提交,等待 SF 事务确认
READY_TO_SHOW可以显示
HAS_DRAWN已经显示过至少一帧

白屏、启动窗口秒关、窗口一闪而过,很多都和这个状态机的跳转有关。应用层觉得“我画完了”,WMS 那边可能还在等 buffer 真正到达 SF。

5. 实战排错:窗口没出来的问题,怎么按代码流程定位

5.1 先看 dumpsys window 的窗口状态

窗口相关的问题,我排查时第一件事永远是dumpsys window windows。不要瞎猜,先看系统侧认为这个窗口存在不存在。

adb shell dumpsys window windows

输出里重点看几个字段:

  • mWindowInfo或Window #...:窗口是否存在;
  • mViewVisibility:应用侧视图是否可见;
  • mHasSurface:Surface 是否已创建;
  • mDrawState:当前处在哪个绘制状态;
  • mFrame:窗口最终位置大小。

如果WindowState根本没有,说明addWindow就没通过校验。如果mHasSurface=false,问题在 relayout 之前的链路。如果mHasSurface=true但mDrawState一直停在DRAW_PENDING,那就是应用提交第一帧慢了,问题不在 WMS,而在应用绘制本身。

这一步能把问题范围从“整个窗口链路”直接缩小到具体阶段,非常省时间。

5.2 卡顿、延迟和白屏时的链路检查

窗口打开慢,一般有两种形态:一种是整体延迟,比如点了按钮后几百毫秒才看到窗口;另一种是白屏,窗口出现了但内容迟迟不来。

整体延迟优先怀疑 Binder 同步阻塞。addToDisplay和relayout都是同步调用,如果 WMS 那边mGlobalLock被某个耗时任务占住,所有窗口的请求都会排队。这时候用 systrace 抓取能看到系统进程里 WMS 的 Binder 调用耗时异常长。

白屏优先怀疑绘制链路。Surface 已经是有效的,但应用侧迟迟没有 buffer 提交。可以通过 systrace 或 Perfetto 看:

  • 应用进程有没有执行draw;
  • HardwareRenderer有没有提交 buffer;
  • SurfaceFlinger 有没有收到新 buffer 并合成。

我自己遇到的一次典型问题,是应用在onCreate里做了大量耗时初始化,导致第一帧提交前主线程一直被占用。从流程上,WMS 那边窗口已经DRAW_PENDING等了两百多毫秒,体验上就是白屏。

5.3 BadTokenException 与权限拒绝的源头理解

BadTokenException 是窗口开发里最常见的异常,但很多人只会在应用层打日志,不知道真正判断在系统进程的addWindow里。如果你用LayoutParams设置了一个已经失效的 token,WMS 在校验阶段就直接返回ADD_BAD_APP_TOKEN。

排查方法很简单:日志里搜WindowManagerService关键字,能看到类似:

WindowManager: addWindow: window already exists WindowManager: addWindow: Bad token, window token=...

权限问题也是同一套路。TYPE_APPLICATION_OVERLAY这类窗口在 Android 8.0 以后要求弹窗权限,WMS 在addWindow里会检查mContext.checkCallingPermission。如果权限没有授予,返回ADD_PERMISSION_DENIED,应用层再抛SecurityException。

这类问题的定位诀窍是:不要只看异常堆栈,先确认 Binder 调用是否真正到达了 WMS。很多时候应用主动做了 try-catch,异常被吞掉,但dumpsys window里窗口列表始终没有目标窗口。从这段代码流程来看,窗口没出现在dumpsys里,说明问题在addWindow之前的任何一步,而不是在绘制和合成阶段。

窗口打开的代码流程,说复杂也复杂,说简单也简单:应用提交一个 View 树,WMS 负责登记和算位置,SF 负责把内容合成出去。中间每一层都有各自的校验和状态,排错时只要能确定“问题发生在哪一层”,就成功了一大半。每次遇到窗口问题,我建议先把dumpsys window的状态和异常堆栈对应起来看,再决定是抓 systrace 还是查权限。这套方法我用了很多年,比对着源码瞎猜要靠谱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询