监听器和观察者未注销必然导致内存泄漏,因强引用链钉死Activity等组件,引发级联式内存堆积;需严格配对add/remove、避免跨生命周期注册、慎用匿名内部类与闭包,并在销毁时显式解注册。

监听器和观察者没注销,不是“可能泄漏”,而是大概率已经泄漏——而且往往牵一发而动全身,形成级联式内存堆积。
监听器未注销:强引用链直接钉死Activity
Android中,View.setOnClickListener、MediaPlayer.addListener、ViewTreeObserver.addOnGlobalLayoutListener 等,注册后都会隐式持有调用方(比如Activity)的强引用。一旦Activity进入onDestroy(),但监听器还挂在系统对象上,GC就无法回收它。
更危险的是“跨生命周期残留”:比如在Fragment里给全局播放器注册监听器,Fragment销毁了,监听器还在;或者在Dialog里注册了ViewTreeObserver回调,Dialog dismiss了,但布局监听仍在触发——这些都会让整个UI组件树卡在内存里。
- 检查所有 addXXXListener 的位置,确认对应 removeXXXListener 是否成对出现
- 特别关注异步场景:如网络回调成功后更新UI,若此时Activity已finish,回调里的this仍被持有
- 使用弱引用包装上下文(WeakReference
)可缓解,但不能替代显式注销
观察者模式中的“单向绑定”陷阱
很多自定义观察者实现(尤其手写Subject-Observer)只做attach,不做detach。主题(Subject)用ArrayList或HashSet存着Observer,而Observer又持有Activity/Fragment引用——结果就是主题不销毁,Observer永远不释放,Activity也跟着陪葬。
Python里可用WeakSet自动管理;Java/Kotlin中没有原生弱集合支持,必须手动维护生命周期。常见错误包括:
- 单例类持有一个静态Subject,长期存活,导致所有注册过的Observer都无法回收
- 使用匿名内部类Observer,隐式持有外部Activity,且未提供remove接口
- LiveData.observe(this, ...)看似安全,但若this是Service或非LifecycleOwner,照样泄漏
事件多播委托(C#)与闭包(JS/TS/Kotlin)的隐式捕获
C#中+=订阅事件,本质是往_multicastDelegate._invocationList里加一个带_target(实例)和_methodPtr的委托对象。只要这个委托还在列表里,_target就被强引用锁定。
JavaScript里,addEventListener绑定的回调若用了闭包,会把外层函数作用域里所有变量(包括DOM节点、大数组、甚至整个Vue实例)全锁住;Kotlin里,lambda默认持有this,协程launch { } 中的this若指向Activity,也会拖慢回收。
- C#:务必在Dispose()或OnDestroy()中执行 -=,推荐配合IDisposable模式
- JS:移除DOM前先removeEventListener;一次性事件优先用{ once: true }
- Kotlin:避免在lambda中直接访问activity成员,改用weakRef.get()?.let { … }
级联泄漏的典型路径
一个没注销的监听器,可能引发三重泄漏:
Activity → View → ViewTreeObserver → OnGlobalLayoutListener → Activity(闭环)Fragment → ViewModel → LiveData → Observer(匿名)→ Fragment(强持)
Service → EventBus → Subscriber → Activity(未解注册)
这种环状或长链引用,会让GC Roots始终连通,哪怕表面看只是“少调了一行remove”。MAT或Android Profiler里常看到大量$OnClickListener、$Observer实例堆积,就是这类问题的铁证。

















