刚跑完一个压力测试,存泄App 在后台挂了 20 分钟,存泄回来直接白屏。存泄Logcat 里一串 GC 日志,存泄堆内存从 64M 一路涨到 256M,存泄最后 OOM。存泄不用查了,存泄内存泄漏。存泄
Android 的存泄内存泄漏跟 C++ 那种不一样。C++ 是存泄 malloc 了没 free,明明白白。存泄Android 是存泄 GC 觉得这对象还有人用,不敢收,存泄实际上早该死了。存泄GC 的存泄垃圾回收机制是基于可达性分析的,从 GC Roots 出发,凡是还能被引用到的对象就不会被回收。问题就出在这个“还能被引用到”上。
最常见的场景:一个 Activity 被 finish 了,但某个静态变量或者单例还持有它的引用。GC 一看,哟,还有人牵着,不收。于是整个 Activity 连带它里面的 View 树、Bitmap、Context,全卡在堆里。一个 Activity 少说占几 M,来回旋转屏幕几次,堆就爆了。
我见过最离谱的一个案例。某项目里有个静态的 List<Callback>,每次 Activity 创建时就把自己注册进去,onDestroy 的时候忘了移除。用户正常使用,打开关闭同一个页面十几次,堆里就躺了十几个 Activity 实例。用 MAT 打开 hprof 文件一看,Dominator Tree 里那个 List 下面挂了一长串,跟葡萄似的。
LeakCanary 是必备工具。集成进去,debug 包跑起来,泄漏了直接通知栏弹出来,引用链给你列得清清楚楚。但有个坑:LeakCanary 只能检测 Activity 和 Fragment 的泄漏,其他对象它不管。而且有些泄漏是系统框架层的,比如 InputMethodManager 在某些版本上会持有 View 的引用,这种你改不了,只能绕。
Handler 也是重灾区。非静态内部类或者匿名类会隐式持有外部类的引用。你写个 Handler 做延时任务,延时 10 分钟,用户 2 分钟就退了页面。MessageQueue 里的 Message 还持有 Handler,Handler 持有 Activity,GC 干瞪眼。解决办法要么用静态内部类加 WeakReference,要么在 onDestroy 里 removeCallbacksAndMessages(null)。我倾向于后者,简单粗暴。
单例模式听起来人畜无害,实际上经常出事。比如一个 AppManager 单例,里面存了个 currentActivity 的引用。Activity 切换的时候没更新,旧的 Activity 就一直被单例攥着。还有 EventBus 这种,注册了没反注册,或者反注册写在了 onDestroy 里但 onDestroy 根本没被调用——比如 Activity 被系统回收了。
匿名内部类做回调也是经典陷阱。比如开个线程做网络请求,回调里更新 UI,匿名 Runnable 持有 Activity 引用。请求还没回来,Activity 先没了。线程还在跑,引用还在,内存就漏了。这种用 WeakReference 包一层,或者用 RxJava 的 CompositeDisposable 统一管理,onDestroy 的时候 dispose 掉。
资源没关也算。Cursor、File、Stream,用完不 close,底层 native 内存泄漏,Java 堆看着正常,native 堆一直涨。这种用 try-with-resources 或者 finally 里关,别偷懒。
说白了,内存泄漏的本质就是生命周期对不上。长生命周期的对象持有了短生命周期对象的引用,短的那个该走的时候走不了。写代码的时候多想想:这个引用,谁持有了它,持有者能活多久,被持有者该活多久。想清楚了,大部分泄漏都能避免。
MAT 和 LeakCanary 是事后排查,真正省事的办法是写的时候就注意。静态变量、单例、Handler、匿名内部类、资源对象,这几个地方多留个心眼。代码 review 的时候看到 static 修饰的集合或者 Context,直接问一句:这里会不会泄漏?