3、WindowManager 的获取方式:通过 Context.getSystemService() 获取、WindowManagerImpl 与 WindowManagerGlobal 的关系
好,咱们今天来聊聊 WindowManager 是怎么拿到手的。
很多人写 Android 代码,上来就是 getSystemService(Context.WINDOW_SERVICE),然后强转成 WindowManager,完事。但你真的清楚这背后发生了什么吗?
我个人习惯是,在学一个系统服务之前,先搞清楚它的“入口”长什么样。因为入口决定了你能调什么 API,也决定了你踩坑的姿势。
3.1 从 Context 到 WindowManager
咱们先看最常用的写法:
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
这行代码,说白了就是向系统要一个“窗口管理器”的实例。但这里有个细节——你拿到的到底是谁?
我告诉你,你拿到的不是 WindowManager 接口本身,而是它的实现类 WindowManagerImpl。
为什么会这样?因为 Context.getSystemService() 内部走的是 ServiceFetcher 机制。每个系统服务都有一个对应的 fetcher,WindowManager 的 fetcher 在 SystemServiceRegistry 里注册好了。当你调用时,它返回的就是 WindowManagerImpl 实例。
嗯,这里要注意:不同 Context 拿到的 WindowManagerImpl 是同一个吗?
不是。Activity 和 Application 拿到的 WindowManagerImpl 实例不同,因为它们的父窗口(parent window)不同。Activity 的 WindowManagerImpl 绑定的是 Activity 的 PhoneWindow,而 Application 的则没有。
核心结论:getSystemService() 返回的是 WindowManagerImpl 实例,不是 WindowManagerGlobal。
3.2 WindowManagerImpl 与 WindowManagerGlobal 的关系
好,现在咱们拿到了 WindowManagerImpl。那它跟 WindowManagerGlobal 是什么关系?
我打个比方你就明白了:
- WindowManagerImpl 是前台接待,你直接跟它打交道。
- WindowManagerGlobal 是后台总管,所有脏活累活都是它干。
说白了,WindowManagerImpl 就是个代理类。它把 addView、removeView、updateViewLayout 这些操作,全部委托给了 WindowManagerGlobal 去执行。
咱们看看源码就清楚了:
// WindowManagerImpl.java
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
mGlobal.updateViewLayout(view, params);
}
看到了吗?mGlobal 就是 WindowManagerGlobal 的单例。所有 WindowManagerImpl 实例共享同一个 mGlobal。
我记得有一次排查线上崩溃,发现某个页面 addView 报错“View not attached to window manager”。查了半天,发现是因为两个不同的 WindowManagerImpl 实例在操作同一个 View。嗯,这就是没搞懂它们关系导致的坑。
避坑指南:我曾经遇到过,在 Service 里通过 Application Context 获取 WindowManager 来添加悬浮窗,结果发现 addView 总是失败。原因就是 Application 的 WindowManagerImpl 没有关联的 PhoneWindow,导致 token 校验不通过。解决方案是使用 TYPE_APPLICATION_OVERLAY 并申请权限。
3.3 整体架构图
下面这张图,我画了它们之间的关系,你看一眼就明白了:
从这张图你能看到:
- 应用层通过 Context 拿到 WindowManagerImpl
- WindowManagerImpl 把操作委托给 WindowManagerGlobal
- WindowManagerGlobal 内部维护了四个数组:mViews、mRoots、mParams、mDyingViews
你想想看,为什么 Google 要这么设计?
说白了,就是为了解耦。WindowManagerImpl 可以有很多个(每个 Activity、Dialog 都有自己的),但真正干活的 WindowManagerGlobal 只有一个。这样既保证了每个窗口的独立性,又避免了重复造轮子。
3.4 关键数据结构一览
WindowManagerGlobal 内部维护的四个数组,我整理成了表格:
| 数组名 | 类型 | 作用 |
|---|---|---|
| mViews | View[] | 存储所有添加的 View(即窗口的根 View) |
| mRoots | ViewRootImpl[] | 每个 View 对应的 ViewRootImpl,负责绘制和事件分发 |
| mParams | WindowManager.LayoutParams[] | 每个窗口的布局参数(位置、大小、类型等) |
| mDyingViews | View[] | 正在执行移除动画的 View,动画结束后真正移除 |
这四个数组的下标是一一对应的。也就是说,mViews[i]、mRoots[i]、mParams[i] 描述的是同一个窗口。
注意:千万不要在多个线程中同时操作 addView 和 removeView。虽然 WindowManagerGlobal 内部加了锁,但如果你在回调里又去操作同一个 View,很容易出现数组越界或空指针。我曾经就在 onGlobalLayout 里直接 removeView,结果 crash 了——因为那时候 ViewRootImpl 还在处理上一轮的消息。
3.5 小结
好,咱们总结一下:
- 通过
Context.getSystemService()拿到的是 WindowManagerImpl - WindowManagerImpl 是代理,真正的实现是 WindowManagerGlobal(单例)
- WindowManagerGlobal 内部用四个数组管理所有窗口
- 不同 Context 拿到的 WindowManagerImpl 实例不同,但共享同一个 WindowManagerGlobal
嗯,这一节的内容就到这里。下一节咱们会深入 ViewRootImpl,看看窗口是怎么被添加到屏幕上的。