3. WindowManager 的获取方式:getSystemService、Context 与 Activity 中的不同获取路径
好,咱们今天聊一个看似简单、实则暗藏玄机的话题——WindowManager 到底怎么拿?
你可能觉得,这有什么好讲的?不就是 getSystemService(WINDOW_SERVICE) 吗?嗯,没错,但问题在于——从谁身上调这个方法,结果可能完全不一样。
我刚开始做 Android 开发那会儿,就踩过这个坑。明明拿到了 WindowManager,往上面加了个 View,结果怎么都不显示。后来才发现,是我拿错了对象。今天咱们就把这事彻底捋清楚。
3.1 三种常见的获取方式
在 Android 里,获取 WindowManager 主要有三条路:
- 从 Activity 获取:
getWindowManager() - 从 Context 获取:
context.getSystemService(Context.WINDOW_SERVICE) - 从 Application 获取:
application.getSystemService(...)
这三条路,最终返回的都是 WindowManagerImpl 的实例。但它们的内部状态不一样。
核心区别:Activity 的 WindowManager 绑定了 Activity 自身的 Window 和 Token。而 Application 或普通 Context 拿到的,是一个「独立」的 WindowManager,没有关联任何 Activity 的窗口令牌。
3.2 源码层面看差异
咱们直接看源码。Activity 里 getWindowManager() 是怎么实现的?
// Activity.java
public WindowManager getWindowManager() {
return mWindowManager;
}
// 这个 mWindowManager 是在 attach() 方法里赋值的
final void attach(Context context, ...) {
mWindowManager = mWindow.getWindowManager();
}
而 mWindow.getWindowManager() 返回的是 WindowManagerImpl,但注意——这个 WindowManagerImpl 内部持有的是 Activity 的 Window 对象。
再看看 Context 的路径:
// ContextImpl.java
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
return getWindowManager();
}
...
}
private WindowManager getWindowManager() {
if (mWindowManager == null) {
mWindowManager = new WindowManagerImpl(mMainThread.getDisplay(), null);
}
return mWindowManager;
}
看到了吗?第二个参数传的是 null。这个参数就是 Window 对象。Activity 版本传的是 mWindow,普通 Context 传的是 null。
我的经验:如果你在 Service 或 BroadcastReceiver 里通过 getSystemService 拿到 WindowManager,然后试图往上面添加一个需要 TYPE_APPLICATION_OVERLAY 类型的 View,大概率会失败。因为缺少了 Activity 的 token 校验。
3.3 不同场景下的选择策略
我个人习惯这样区分:
| 场景 | 推荐获取方式 | 原因 |
|---|---|---|
| 在 Activity 内添加子 View | getWindowManager() |
直接关联当前 Activity 的窗口,token 完整 |
| 在 Fragment 内操作窗口 | requireActivity().getWindowManager() |
确保拿到的是宿主 Activity 的 WindowManager |
| 在 Service 中创建悬浮窗 | getSystemService(WINDOW_SERVICE) |
需要配合 TYPE_APPLICATION_OVERLAY 和权限 |
| 在 Application 中全局使用 | application.getSystemService(...) |
不依赖任何 Activity 生命周期,但功能受限 |
3.4 避坑指南:我曾经踩过的雷
我曾经在一个自定义 Dialog 里,通过 context.getSystemService() 拿到了 WindowManager,然后调用 addView() 添加了一个全屏的遮罩层。结果在 Android 10 以上的设备上,直接抛出了异常:
android.view.WindowManager$BadTokenException:
Unable to add window -- token null is not valid; is your activity running?
为什么会这样?因为 Dialog 的 context 可能是 ApplicationContext。如果你在 Dialog 的构造里传了 ApplicationContext,那它拿到的 WindowManager 就没有 token。
重要提醒:addView() 方法需要 token 的场景包括:
- 使用
TYPE_APPLICATION类型的窗口 - 使用
TYPE_APPLICATION_OVERLAY且 targetSdk >= 29 - 任何需要窗口焦点的场景
没有 token,系统不知道这个窗口属于哪个应用,直接拒绝。
3.5 知识体系图
下面这张图,把 WindowManager 的获取路径和内部关系画清楚了:
3.6 实际项目中的建议
说了这么多,总结几条实战经验:
- 能用 Activity 的,就别用 Context 的。这是最稳妥的做法。
- 如果你在非 Activity 场景下需要 WindowManager(比如 Service 里做悬浮窗),记得检查 Android 版本和权限。Android 10 以后对悬浮窗限制很严。
- Dialog 里操作 WindowManager 要格外小心。我建议你在 Dialog 的
onAttach()之后再去获取,确保 context 是 Activity。 - 不要缓存 WindowManager 实例。Activity 销毁后,它持有的 Window 对象就失效了。每次需要时重新获取。
一个小技巧:如果你不确定当前拿到的 WindowManager 是否有效,可以调用 windowManager.getDefaultDisplay() 看看会不会抛异常。如果抛了,说明这个实例已经废了。
嗯,关于获取方式,今天就聊到这儿。记住一句话:WindowManager 本身只是个壳,真正重要的是它背后绑定的 Window 和 Token。下次咱们深入看看 addView() 到底干了什么。