4. Android内存泄漏常见模式:四大组件、视图、线程场景解析
内存泄漏这玩意儿,说白了就是对象本该被回收,结果因为有人还拽着它不放,导致GC干瞪眼。我排过不少线上OOM问题,十有八九都能归到几个固定的“坑”里。今天咱们就把四大组件、视图、线程这些重灾区挨个过一遍,看看它们到底是怎么漏的。
核心观点:内存泄漏的本质是长生命周期对象持有了短生命周期对象的强引用。
4.1 Activity 泄漏:最常见的“隐形杀手”
Activity 泄漏几乎成了Android内存泄漏的代名词。Activity 本身是个“短命鬼”,onDestroy 之后就应该被回收。但现实生活中,总有人强行留住它。
4.1.1 静态变量持有 Activity
我印象很深,有个同事把 Activity 塞进了一个静态 List 里当缓存用。
// 错误示例:静态List持有Activity
public class ImageCache {
private static List<Activity> sActivities = new ArrayList<>();
public static void addActivity(Activity activity) {
sActivities.add(activity); // Activity被长期持有
}
}
改法很简单:要么不用静态集合存 Activity,要么改用WeakReference<Activity>。
4.1.2 内部类/匿名内部类隐式持有
非静态内部类会隐式持有外部类的引用,这是Java语法层面的特点。当你的 Activity 里写了个匿名 Runnable,并把这个 Runnable 丢给后台线程执行,Activity 就“被绑票”了。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
// 长时间操作,Runnable持有Activity引用
SystemClock.sleep(10000);
}
}).start();
}
}
Activity 都 finish 了,线程还在跑,Runnable 还拽着 Activity 不放——GC 想回收都没法下手。我个人习惯遇到这种情况,要么用静态内部类+弱引用,要么在 onDestroy 里把线程停掉。
小技巧:Android Studio 的 Profiler 里,如果看到 Activity 实例数只增不减,十有八九是用静态变量或者内部类把它“焊死”了。
4.2 Service 泄漏:默默吃内存的“后台服务”
Service 的问题往往不是它自己漏,而是它里面开的线程漏。Service 本身生命周期很长(尤其是前台服务),如果它内部启动了一个 HandlerThread 或者 AsyncTask,又不加管理,Activity 退出后 Service 还在跑,线程里的对象就泄漏了。
我曾经排查过一个音乐播放App:用户切出去半小时后,内存飙到400MB。一查发现 Service 里的 Handler 用了个匿名内部类,持有 Service 引用,而 Service 又被系统绑定着——整个引用链像铁链一样串起来。
解决方案:
- Service 中尽量使用静态内部类+弱引用
- onDestroy 时停止所有线程,清除消息队列(Handler.removeCallbacksAndMessages(null))
- 考虑用 IntentService 处理一次性任务
4.3 BroadcastReceiver 与 ContentProvider 泄漏
这两个组件泄漏一般比较隐蔽。
- BroadcastReceiver:动态注册后必须调用
unregisterReceiver(),否则持有 Activity/Service 引用。我见过很多人在 onResume 注册、onPause 忘了解注册的场景。 - ContentProvider:如果用了 Cursor,一定要确保在 finally 块里 close。Cursor 内部持有 ContentResolver 的引用,连着连着一大串都没法回收。
注意:onDestroy 里不要依赖系统帮你解注册,有时候 Activity 因为异常销毁可能不会走 onDestroy。我建议在 onPause 或 onStop 里就进行解注册操作,更稳妥。
4.4 视图(View)泄漏:UI 组件的“藏匿点”
View 本身被 Activity 持有,但问题往往出在 View 被“移花接木”到了其他地方。
4.4.1 Drawable 回调泄漏
当 View 设置了 Drawable,而 Drawable 有一个回调(Callback)指向 View,如果这个 Drawable 被某个静态变量持有,View 和它的 Activity 都会被牵连。
// 典型场景:把View的Background Drawable缓存起来
private static Drawable sCachedDrawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = new View(this);
sCachedDrawable = view.getBackground(); // Drawable持有View的Callback
}
解决:要么不缓存 Drawable,要么在 onDestroy 里把 callback 置空。
4.4.2 匿名回调持有 Activity
比如给 View 设置 OnClickListener,如果这个 listener 是匿名内部类,它隐式持有 Activity。Activity 销毁后如果 View 还被其他模块持有(比如被加入一个静态的 list),就泄漏了。
我一般习惯在 onDestroy() 里把 View 的 listener 清空:
@Override
protected void onDestroy() {
super.onDestroy();
if (myButton != null) {
myButton.setOnClickListener(null);
}
}
4.5 线程与 Handler 泄漏:延时消息的“定时炸弹”
Handler 泄漏是Android经典难题。Handler 内部类持有 Activity,而 Message 又持有 Handler——所以只要 MessageQueue 里还有这条消息,Activity 就永远释放不了。
// 错误写法
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 更新UI
}
};
正确的做法:
- 使用静态内部类 + WeakReference<Activity>
- 在 onStop/onDestroy 里移除所有回调:
mHandler.removeCallbacksAndMessages(null)
还有一个很多人忽略的点:AsyncTask。这东西虽然Android 11已经废弃,但老项目里随处可见。AsyncTask 内部持有一个 FutureTask,如果它在后台执行,而 Activity 销毁了,AsyncTask 的匿名内部类会泄漏 Activity。
我的建议:新项目一律用 Kotlin 协程 + ViewModel,生命周期感知做得更漂亮;老项目遇到 AsyncTask 能改就改,不能改的话务必在 onCancelled 里取消任务。
4.6 集合类与单例泄漏:长期缓存不清理
这个问题本质上和静态变量一样——把东西放进集合后忘了拿出来。
| 场景 | 泄漏原因 | 修复方案 |
|---|---|---|
| 全局集合缓存Fragment | Activity退出后Fragment仍被集合持有 | 使用WeakHashMap或手动移除 |
| 单例中持有多张Bitmap | 图片缓存无限增长 | 设置LRU大小上限,或弱引用 |
| EventListener列表 | 注册后未取消 | 使用弱引用观察者模式 |
还有一个想强调的:单例中传入Context。很多人写工具类时直接传 Activity 的 Context 进去,那整个 Activity 就跟着单例一起活到天荒地老。正确做法是传 ApplicationContext。
4.7 WebView 泄漏:历史遗留的“巨坑”
WebView 的内存泄漏在系统层面就有问题(Android 4.4以下特别严重)。WebView 引擎会持有一个 Activity 的引用,导致 Activity 无法回收。
我曾经在旧项目里遇到一个 WebView 加载了H5页面,用户来回进出8次就OOM了。处理方案:
- 把 WebView 放在单独的 Activity 或者 Fragment 里,用完即销毁
- 在 onDestroy 时调用
webView.destroy()并把引用置空 - 如果 App 频繁使用 WebView,可以考虑用一个全局的 WebView 实例动态附加到不同 Activity(但要注意 Activity 切换时清除状态)
避坑指南:我曾经犯过一个错误——在 Activity 里new了一个 WebView,但没把它 add 到任何 ViewGroup 中。结果这个 WebView 虽然不显示,但依然持有 Activity 引用。所以记住:不需要显示时也要手动 destroy 掉。
4.8 总结:怎么快速定位泄漏模式
以上几类模式,覆盖了90%以上的Android内存泄漏场景。我个人遇到内存问题时,第一步不是看代码,而是先打开 Memory Profiler,看看 Activity 实例数是不是只增不减。一旦确认有泄漏,第二步就是查 GC Roots。
推荐用 Android Studio 的 Analyze Dump 功能,或者用 LeakCanary 自动检测。熟练以后,看到泄漏路径基本就能猜到是哪个模式。
嗯,咱们这一章先聊到这。记住一句话:见到静态变量、内部类、延时消息、单例、Context——心里就得敲警钟。