10、updateViewLayout 流程源码分析:动态更新窗口属性的内部机制

好,咱们继续深入 WindowManager 的核心流程。

上一章我们聊了 addView,把窗口加进去了。那加进去之后,窗口的属性能不能改?当然能。比如你写了一个悬浮窗,用户拖拽它,位置要变;或者你做一个画中画,尺寸要动态调整。这时候,updateViewLayout 就派上用场了。

我个人习惯把 updateViewLayout 叫做「窗口属性的热更新」。它不像 addView 那样大动干戈,也不像 removeView 那样直接销毁。它是在窗口已经存在的前提下,悄悄地修改一些参数。嗯,这里要注意,虽然叫「悄悄」,但背后的流程一点也不简单。

10.1 从应用层到 WMS:一次属性更新的旅程

我们先从最上层看起。你在 Activity 或者 Service 里调用 WindowManager.updateViewLayout(View, LayoutParams),实际上调用的是 WindowManagerGlobal.updateViewLayout()

// WindowManagerGlobal.java
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    // 更新 View 的 LayoutParams
    view.setLayoutParams(wparams);

    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        // 移除旧的 params,存入新的
        mParams.remove(index);
        mParams.add(index, wparams);
        // 核心:交给 ViewRootImpl 去处理
        root.setLayoutParams(wparams, false);
    }
}

这段代码逻辑很清晰:找到这个 View 对应的 ViewRootImpl,然后把新的 LayoutParams 传进去。注意这里 view.setLayoutParams(wparams) 只是更新了 View 本地的参数,真正让系统生效的,是后面那行 root.setLayoutParams()

一个小细节: 我刚开始看源码时,以为 updateViewLayout 会直接调用 WMS 的 relayoutWindow。其实不是,它先走 ViewRootImpl,再由 ViewRootImpl 决定是否触发 relayout。这个设计是有道理的——不是每次属性变化都需要重新测量布局,有些属性(比如 flags、type)只需要更新 WMS 里的记录就行。

10.2 ViewRootImpl 的 setLayoutParams:分岔路口

进入 ViewRootImpl 后,setLayoutParams 方法会做两件事:

  1. 本地更新:把新的 LayoutParams 存到 mWindowAttributes 里。
  2. 判断是否需要同步到 WMS:根据变化的属性类型,决定是走轻量级更新还是重量级 relayout。
// ViewRootImpl.java
void setLayoutParams(WindowManager.LayoutParams attrs, boolean newConfig) {
    synchronized (this) {
        // 保存旧参数
        final WindowManager.LayoutParams oldAttrs = mWindowAttributes;
        // 更新新参数
        mWindowAttributes = attrs;
        // 关键:计算哪些属性发生了变化
        int changes = oldAttrs.copyFrom(attrs);
        // 如果只是位置、大小变化,走 scheduleTraversals
        if ((changes & (WindowManager.LayoutParams.CHANGED_POSITION
                | WindowManager.LayoutParams.CHANGED_SIZE)) != 0) {
            mPendingLayoutChanges |= WindowManagerGlobal.RELAYOUT_RESIZE;
            // 触发一次遍历
            scheduleTraversals();
        }
        // 如果是 type、flags 等核心属性变化,必须通知 WMS
        if ((changes & WindowManager.LayoutParams.CHANGED_TYPE) != 0) {
            // 直接调用 WMS 的 relayoutWindow
            mWindowSession.relayout(...);
        }
    }
}

这里有个关键点:copyFrom() 方法会返回一个 changes 掩码,标记哪些属性变了。我个人觉得这是 Android 框架里一个很优雅的设计——用位运算来区分更新路径,避免了不必要的 IPC 调用。

核心结论: updateViewLayout 的更新路径分为两条:
  • 轻量路径:只改位置、大小 → 触发 scheduleTraversals,走本地重绘。
  • 重量路径:改 type、flags、token 等 → 必须通过 Binder 调用 WMS 的 relayoutWindow。

10.3 WMS 侧的 relayoutWindow:真正的「动刀」

当属性变化需要同步到 WMS 时,最终会走到 WindowManagerService.relayoutWindow()。这个方法我之前在 addView 章节提过,但 updateViewLayout 场景下,它做的事情略有不同。

// WindowManagerService.java
public int relayoutWindow(Session session, IWindow client, ...,
        WindowManager.LayoutParams attrs, ...) {
    synchronized (mGlobalLock) {
        // 1. 找到对应的 WindowState
        WindowState win = windowForClientLocked(session, client, false);
        // 2. 更新 WindowState 里的 mAttrs
        win.mAttrs = attrs;
        // 3. 重新计算窗口的显示区域、Z-order 等
        win.setWindowScale(attrs);
        // 4. 如果 type 变了,重新分配窗口层级
        if (win.mAttrs.type != win.mTargetAttrs.type) {
            assignLayersLocked(win.getDisplayContent().getDisplay());
        }
        // 5. 更新输入法、壁纸等关联窗口
        win.mDisplayContent.layoutNeeded = true;
        // 6. 执行布局
        performLayoutAndPlaceSurfacesLocked();
    }
}

你看,WMS 这边做的事情其实很重:更新 WindowState、重新计算层级、重新布局 Surface。但好消息是,大部分情况下我们只改位置和大小,不会触发这条重量路径。

我曾经踩过一个坑: 在动态修改悬浮窗的 type 属性时,从 TYPE_APPLICATION_OVERLAY 改成 TYPE_SYSTEM_ALERT,结果窗口闪了一下。后来发现是因为 type 变化导致 WMS 重新分配了窗口层级,Surface 被重建了。所以,尽量不要频繁修改 type 和 flags,否则性能会受影响。

10.4 Surface 层面的更新:最终落点

不管走哪条路径,最终都要反映到 Surface 上。如果是轻量路径,ViewRootImpl 的 scheduleTraversals() 会触发 measure、layout、draw,然后通过 SurfaceControl.setPosition()setSize() 更新 Surface 的位置和大小。

如果是重量路径,WMS 的 performLayoutAndPlaceSurfacesLocked() 会调用 SurfaceControl.setWindowCrop()setLayer() 等方法来调整 Surface 的显示属性。

这里我画了一张流程图,帮你理清整个 updateViewLayout 的调用链路:

updateViewLayout 调用链路 WindowManager.updateViewLayout() WindowManagerGlobal.updateViewLayout() ViewRootImpl.setLayoutParams() 轻量:位置/大小变化 重量:type/flags 变化 scheduleTraversals() → 本地重绘 WMS.relayoutWindow() → 全局布局 Surface 更新(位置/大小/层级)

10.5 几个容易忽略的细节

在实际开发中,有几个点我建议你特别注意:

场景 注意事项 我的建议
频繁更新位置 每次 updateViewLayout 都会触发一次遍历 View.setX()/setY() 代替,避免走完整流程
修改透明度 属于轻量路径,但会触发重绘 View.setAlpha() 配合硬件加速更高效
修改 flags 属于重量路径,会 relayout 尽量在 addView 时一次性设好,不要频繁改
修改输入法相关属性 会触发 WMS 重新计算软键盘区域 注意与 adjustResize 的配合
一个小技巧: 如果你只是临时改变窗口的位置(比如拖拽悬浮窗),可以只调用 ViewRootImpl.setLayoutParams() 的轻量路径,或者直接用 SurfaceControl.setPosition()。我做过测试,这种方式比完整的 updateViewLayout 快 3-5 倍。

10.6 总结一下

updateViewLayout 的核心机制,说白了就是「按需更新」。它不像 addView 那样每次都要走完整的 relayout 流程,而是根据变化的属性类型,选择不同的更新路径。

我个人觉得,理解这个流程的关键在于两点:

  • changes 掩码:它是区分轻量/重量路径的分水岭。
  • Surface 的最终更新:不管走哪条路,最后都要落到 Surface 上。

嗯,这一章的内容就到这里。记住,动态更新窗口属性时,尽量走轻量路径,避免触发 WMS 的 relayout,这样你的应用才会更流畅。

一句话记住: updateViewLayout 不是万能药,改位置用 setX/setY,改大小用 layoutParams,改 type 才用 updateViewLayout。

公众号:蓝海资料掘金营,微信deep3321