27、Window 泄漏问题排查:常见的 WindowLeaked 异常原因与解决方案

说实话,做 Android 开发这么多年,WindowLeaked 这个异常我见得实在太多了。每次看到崩溃日志里蹦出 Activity has leaked window,我就知道——又有人忘记销毁 Dialog 或者 PopupWindow 了。

今天咱们就好好聊聊这个“老朋友”。我会结合我自己的踩坑经历,把 Window 泄漏的来龙去脉、常见场景、以及怎么排查根治,一次性讲清楚。

什么是 Window 泄漏?

简单说,就是一个 Window 对象(比如 Dialog、PopupWindow、Toast)已经不再需要了,但它还挂在 WindowManager 上,没有被移除。当 Activity 或 Service 销毁时,这些 Window 就成了“孤儿”,系统就会抛出 WindowLeaked 异常。

核心异常信息:

android.view.WindowLeaked: Activity com.example.MainActivity has leaked window 
  android.widget.PopupWindow$PopupDecorView{...} that was originally added here

嗯,这里要注意:泄漏的不是 Activity,而是 Window。但 Activity 作为 Window 的宿主,一旦被销毁,系统就会替它“擦屁股”——把没清理的 Window 全部 dump 出来,然后抛异常。

常见的泄漏场景

我归纳了 5 种最常见的场景,几乎覆盖了 90% 的 WindowLeaked 问题。

场景 典型代码 泄漏原因
Dialog 未 dismiss dialog.show() 后未调用 dismiss() Activity 销毁时 Dialog 仍显示
PopupWindow 未 dismiss popupWindow.showAtLocation() 后未调用 dismiss() 宿主 View 被移除时 PopupWindow 未清理
Toast 在非 UI 线程调用 子线程直接 Toast.makeText().show() Toast 内部依赖 Handler,线程退出后无法移除
自定义 View 直接 addView windowManager.addView(view, params) 未在 onDetachedFromWindow 中 removeView
Service 中创建 Dialog Service 中 new Dialog(this).show() Service 没有 Activity 的 Window Token

为什么会出现泄漏?

说白了,就是生命周期不同步。Window 的生命周期由 WindowManager 管理,而 Window 的宿主(Activity/Service)由系统管理。当宿主销毁时,WindowManager 不会自动帮你清理——你得自己动手。

举个例子:你打开一个 Dialog,然后按返回键退出 Activity。如果 Dialog 没有在 onPause()onDestroy() 中 dismiss,那这个 Dialog 的 DecorView 就还挂在 WindowManager 上。系统检测到这种情况,就会抛出 WindowLeaked。

我的经验:我曾经在一个 Fragment 中连续 show 了 3 个 Dialog,结果在 Fragment 销毁时只 dismiss 了最后一个。那一次线上崩溃率直接飙了 0.5%。从那以后,我养成了一个习惯——所有 Dialog 和 PopupWindow 都在 onStop() 中统一 dismiss

如何排查 Window 泄漏?

排查方法其实不复杂,我一般按这个步骤来:

  1. 看崩溃日志:找到 WindowLeaked 异常,看它提示的“originally added here”堆栈。这个堆栈会告诉你 Window 是在哪里被 add 的。
  2. 检查生命周期:确认 Window 的 show 和 dismiss 是否成对出现。比如 Dialog 的 show()dismiss() 是否在同一个生命周期回调中。
  3. 使用 dumpsys:运行 adb shell dumpsys window windows,查看当前所有 Window 的状态。如果发现某个 Window 的 mOwner 已经是 null,但 mWindowAdded 还是 true,那就是泄漏了。
  4. LeakCanary 辅助:虽然 LeakCanary 主要检测内存泄漏,但 Window 泄漏往往伴随着内存泄漏。装上它,能帮你快速定位。

注意:不要只依赖崩溃日志。有时候 WindowLeaked 不会直接导致崩溃,而是被系统吃掉,只打印一条 warning。所以建议在开发阶段就开启 StrictModedetectActivityLeaks(),提前发现。

解决方案:从根源上避免

解决 Window 泄漏,核心就一句话:谁创建,谁销毁;生命周期结束前,必须销毁

具体来说,有这几个最佳实践:

  • Dialog 和 PopupWindow:在 onStop()onDestroy() 中统一 dismiss。我个人习惯在 onStop() 中处理,因为 onDestroy() 不一定每次都会调用(比如旋转屏幕时)。
  • 自定义 Window:如果用了 WindowManager.addView(),一定要在 onDetachedFromWindow() 中调用 removeView()
  • Toast:不要在子线程中直接 show Toast。如果必须在子线程中显示,用 Handler 切回主线程。
  • Service 中的 Dialog:Service 本身没有 Window Token,所以不能直接创建 Dialog。建议用 Notification 替代,或者使用 TYPE_APPLICATION_OVERLAY 类型的 Window(需要权限)。

避坑指南:我曾经在 RecyclerView 的 Adapter 中直接 show 了一个 PopupWindow,结果因为 View 被复用,PopupWindow 的 dismiss 回调没有被正确触发。后来我改用 PopupWindow.OnDismissListener 来确保每次 dismiss 都清理干净。

SVG 流程图:Window 泄漏的完整链路

Window 泄漏链路图 Activity 创建 show Dialog dismiss Dialog OK Activity 创建 show Dialog Activity 销毁 泄漏 Window 仍挂在 WindowManager 上 抛出 WindowLeaked 异常 正常流程 泄漏流程

总结

Window 泄漏其实不难避免,关键是要有生命周期意识。每次 show 一个 Window,都要问自己一句:这个 Window 会在宿主销毁前被 dismiss 吗?

我个人习惯在 BaseActivity 或 BaseFragment 中统一管理 Dialog 和 PopupWindow 的生命周期。比如在 onStop() 中遍历所有已显示的 Window,逐个 dismiss。这样即使某个地方忘了 dismiss,也不会泄漏。

嗯,最后再提醒一句:不要依赖系统帮你清理。系统确实会在 Activity 销毁时尝试移除所有 Window,但那已经是“事后补救”了。最好的方式,还是自己动手,丰衣足食。