4. WindowManager 核心接口解析:addView()、updateViewLayout()、removeView() 的调用流程与参数详解

说实话,WindowManager 这个类,你平时可能用得不多。但只要是做 UI 开发,尤其是做悬浮窗、弹窗、或者系统级界面,你就绕不开它。它的核心就三个方法:addView()updateViewLayout()removeView()。今天我就带你把这仨兄弟彻底搞明白。

4.1 三个方法,各自管什么?

先给个直观印象:

方法 作用 调用时机
addView() 把一个 View 添加到屏幕上 首次显示时
updateViewLayout() 更新已有 View 的位置、大小、参数 需要调整时
removeView() 从屏幕上移除 View 不再需要时

嗯,看起来很简单对吧?但实际用起来,坑不少。我一个个说。

4.2 addView() —— 把 View 挂到屏幕上

先看签名:

public void addView(View view, ViewGroup.LayoutParams params)

参数就两个:你要显示的 View,以及它的布局参数。但这里有个关键点——params 必须是 WindowManager.LayoutParams 类型,不能是普通的 MarginLayoutParams 或者 FrameLayout.LayoutParams。

核心要点:addView() 内部会创建一个 ViewRootImpl,然后通过它把 View 和 WindowManagerService 连接起来。说白了,就是给 View 找了个"管家"。

我个人习惯在 addView 之前,先检查一下 View 是否已经被添加过。否则会抛异常:

if (view.getParent() == null) {
    windowManager.addView(myView, params);
} else {
    // 已经添加过了,别重复加
}

小技巧:如果你需要频繁显示/隐藏悬浮窗,不要每次都 addView/removeView。用 updateViewLayout 把 visibility 改成 GONE/VISIBLE,性能好很多。

4.3 updateViewLayout() —— 动态调整,别重建

签名:

public void updateViewLayout(View view, ViewGroup.LayoutParams params)

这个方法不会重新创建 ViewRootImpl,只是把新的 LayoutParams 传进去,然后触发 relayout。所以效率比 removeView 再加回来高得多。

我曾经在做一个视频悬浮窗时,用户拖拽窗口位置,每秒要更新几十次。一开始我用 removeView + addView,结果卡成 PPT。后来改成 updateViewLayout,流畅多了。

注意:updateViewLayout 时,传入的 params 必须和 addView 时是同一个类型,而且不能随意改 type 字段。比如你一开始用 TYPE_APPLICATION_OVERLAY,中途改成 TYPE_SYSTEM_ALERT,会直接崩溃。

4.4 removeView() —— 优雅地收尾

签名:

public void removeView(View view)

这个方法会触发 View 的 detach 流程,然后通知 WMS 移除对应的 WindowToken。如果你不调这个方法,View 会一直留在屏幕上,即使 Activity 销毁了也一样。

嗯,这里有个坑:removeView 必须在主线程调用。否则会抛 CalledFromWrongThreadException。为什么?因为 View 的绘制和事件分发都要求在主线程。

我曾经在后台线程收到网络回调后直接调 removeView,结果 crash 了。后来加了个 Handler.post 才搞定。

4.5 调用流程全景图

下面这张图,把三个方法的调用链路画清楚了。你看一眼就明白它们是怎么跟 WMS 打交道的:

WindowManager 核心方法调用流程 addView() updateViewLayout() removeView() 创建 ViewRootImpl 更新 LayoutParams 销毁 ViewRootImpl WindowManagerService SurfaceFlinger 合成

你看,三个方法最终都汇聚到 WMS。区别在于:addView 是"新建",updateViewLayout 是"修改",removeView 是"销毁"。WMS 收到请求后,会更新内部的状态机,然后通知 SurfaceFlinger 去重新合成画面。

4.6 参数详解:WindowManager.LayoutParams

这个类里字段很多,但常用的就这几个:

字段 类型 说明
type int 窗口类型,如 TYPE_APPLICATION、TYPE_APPLICATION_OVERLAY
flags int 窗口行为标志,如 FLAG_NOT_FOCUSABLE、FLAG_KEEP_SCREEN_ON
format int 像素格式,如 PixelFormat.TRANSLUCENT
gravity int 对齐方式,如 Gravity.TOP | Gravity.LEFT
x, y int 窗口左上角坐标
width, height int 窗口宽高,可用 WRAP_CONTENT 或 MATCH_PARENT

避坑指南:type 字段在 Android 10 以后限制很严。非系统应用只能用 TYPE_APPLICATION_OVERLAY,否则会被系统拦截。我曾经在适配 Android 11 时,就因为用了 TYPE_PHONE,结果悬浮窗死活显示不出来。

4.7 一个完整的例子

最后,给你一个完整的悬浮窗示例,把三个方法串起来:

// 1. 创建参数
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
    WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
    PixelFormat.TRANSLUCENT
);
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 100;
params.y = 200;

// 2. 添加
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
View floatView = LayoutInflater.from(context).inflate(R.layout.float_view, null);
wm.addView(floatView, params);

// 3. 更新位置
params.x = 300;
params.y = 400;
wm.updateViewLayout(floatView, params);

// 4. 移除
wm.removeView(floatView);

嗯,代码不多,但每一步都有讲究。尤其是 type 和 flags 的组合,稍不注意就会出问题。我建议你每次加新窗口时,先在官方文档里查一下对应 Android 版本的限制。

好了,三个核心方法就讲到这里。记住一句话:addView 是生,removeView 是死,updateViewLayout 是活着的时候调整姿势。搞明白这个,WindowManager 你就入门了。


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