ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android窗口打开全流程:addView到SurfaceFlinger上屏

2026/10/2 3:29:44 拓冰建站 浏览量
Android窗口打开全流程:addView到SurfaceFlinger上屏 说实话窗口这套东西在 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 中的跨进程调用与 IWindowViewRootImpl.setView是真正开始跟系统交互的地方。它的流程可以压缩成三步把 View 挂到ViewRootImpl的 View 层级里创建ViewRootImpl.W这个IWindow.StubBinder 对象它代表“应用进程里的这个窗口”的远端接口通过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系统进程回调应用侧窗口事件的 BinderLayoutParams 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开头的校验分三块token 校验type为应用窗口时LayoutParams.token必须是有效的IApplicationTokentype是子窗口1000-1999时必须有合法的父窗口。这里最容易挂的就是BadTokenException。type 校验窗口 type 必须落在应用窗口1-99、子窗口1000-1999、系统窗口2000的合理区间内。权限校验一些特殊 type 需要权限比如TYPE_APPLICATION_OVERLAY需要SYSTEM_ALERT_WINDOW注入事件窗口需要INJECT_EVENTS权限。校验失败典型原因对应异常token 无效Activity 已销毁但还拿 token 去加子窗口BadTokenException父窗口不存在子窗口先于父窗口 addViewBadTokenException权限不足未声明SYSTEM_ALERT_WINDOW就弹悬浮窗SecurityExceptionDisplay 不存在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 往返addToDisplay注册窗口返回初始 Framerelayout根据应用请求宽高和系统策略计算最终布局结果并返回 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 遍历的总调度顺序很讲究先 relayout拿到frame和 Insets再performMeasure用窗口真实尺寸约束测量然后performLayout最后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 驱动链路是Choreographer收到 vsync 信号回调ViewRootImpl.TraversalRunnable执行performTraversals和performDrawHardwareRenderer渲染完成后syncAndDrawFrameSurface提交 buffer 到 BufferQueueSurfaceFlinger 在下一次合成时读取 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窗口还没创建 SurfaceDRAW_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应用侧视图是否可见mHasSurfaceSurface 是否已创建mDrawState当前处在哪个绘制状态mFrame窗口最终位置大小。如果WindowState根本没有说明addWindow就没通过校验。如果mHasSurfacefalse问题在 relayout 之前的链路。如果mHasSurfacetrue但mDrawState一直停在DRAW_PENDING那就是应用提交第一帧慢了问题不在 WMS而在应用绘制本身。这一步能把问题范围从“整个窗口链路”直接缩小到具体阶段非常省时间。5.2 卡顿、延迟和白屏时的链路检查窗口打开慢一般有两种形态一种是整体延迟比如点了按钮后几百毫秒才看到窗口另一种是白屏窗口出现了但内容迟迟不来。整体延迟优先怀疑 Binder 同步阻塞。addToDisplay和relayout都是同步调用如果 WMS 那边mGlobalLock被某个耗时任务占住所有窗口的请求都会排队。这时候用 systrace 抓取能看到系统进程里 WMS 的 Binder 调用耗时异常长。白屏优先怀疑绘制链路。Surface 已经是有效的但应用侧迟迟没有 buffer 提交。可以通过 systrace 或 Perfetto 看应用进程有没有执行drawHardwareRenderer有没有提交 bufferSurfaceFlinger 有没有收到新 buffer 并合成。我自己遇到的一次典型问题是应用在onCreate里做了大量耗时初始化导致第一帧提交前主线程一直被占用。从流程上WMS 那边窗口已经DRAW_PENDING等了两百多毫秒体验上就是白屏。5.3 BadTokenException 与权限拒绝的源头理解BadTokenException 是窗口开发里最常见的异常但很多人只会在应用层打日志不知道真正判断在系统进程的addWindow里。如果你用LayoutParams设置了一个已经失效的 tokenWMS 在校验阶段就直接返回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 还是查权限。这套方法我用了很多年比对着源码瞎猜要靠谱得多。