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 自身的 WindowToken。而 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 的获取路径和内部关系画清楚了:

WindowManager 获取路径与内部结构 Activity.getWindowManager() Context.getSystemService() Application.getSystemService() WindowManagerImpl WindowManagerImpl 内部关键字段 mWindow (Window 对象) Activity 持有 ✅ Context/App 持有 ❌ mDisplay (Display) 三者都有 ✅ mParentWindow Activity 持有 ✅ Context/App 持有 ❌ 结论:Activity 获取的 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() 到底干了什么。


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