2、常见泄漏模式:静态变量持有Activity、匿名内部类隐式引用、Handler导致泄漏
内存泄漏这东西,说白了就是本该被回收的对象,因为被不该持有它的对象拽着,导致GC没法处理。我这些年排查过的泄漏案例,十有八九都逃不出这三种模式。今天咱们就把它们掰开揉碎了讲清楚。
2.1 静态变量持有Activity
先看一个最经典的场景。你写了个工具类,里面放了个静态变量,不小心把Activity传进去了。嗯,这就是泄漏的起点。
public class LeakExample {
private static Context sContext;
public static void init(Context context) {
sContext = context; // 这里传入了Activity
}
}
为什么会泄漏?静态变量的生命周期跟Application一样长。Activity退出后,这个静态引用还死死拽着它。GC一看,有人引用着呢,不能回收。于是Activity就永远躺在内存里了。
我在项目中遇到过类似情况。有个同事写了个图片加载工具,为了方便,直接把Activity的Context存成了静态变量。结果用户反复进出某个页面,内存蹭蹭往上涨。最后用MAT一分析,好家伙,十几个Activity实例全挂着。
核心原则:静态变量不要直接或间接持有Activity、Fragment、View等生命周期短的对象。如果非要持有Context,请用ApplicationContext。
修复方案其实很简单:
public class SafeExample {
private static Context sContext;
public static void init(Context context) {
// 只保留ApplicationContext
sContext = context.getApplicationContext();
}
// 或者干脆不用静态变量,用弱引用
private static WeakReference<Context> sContextRef;
public static void initSafe(Context context) {
sContextRef = new WeakReference<>(context.getApplicationContext());
}
}
我的习惯:能用局部变量就别用静态变量。实在要用,记得在不需要时置为null。比如在Activity的onDestroy里调用LeakExample.release()。
2.2 匿名内部类隐式引用
这个坑我踩过不止一次。匿名内部类看起来方便,但它会隐式持有外部类的引用。你想想看,一个匿名Runnable被丢到线程池里执行,线程跑完了才能释放。如果这个Runnable是在Activity里创建的,那Activity就得一直等着。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 匿名内部类,隐式持有MainActivity.this
new Thread(new Runnable() {
@Override
public void run() {
// 模拟耗时操作
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
用户按返回键退出了Activity,但这个线程还在跑。线程里的Runnable对象持有Activity引用,Activity就没法回收。10秒后线程结束,Runnable被释放,Activity才能被GC处理。这10秒里,Activity就白白占着内存。
我曾经在一个社交App里排查过类似问题。用户发帖时上传图片,后台开了个线程做压缩。结果用户快速退出重进,Activity实例越积越多。最后定位到就是匿名内部类搞的鬼。
修复方案:用静态内部类+弱引用。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 使用静态内部类
new Thread(new MyRunnable(this)).start();
}
private static class MyRunnable implements Runnable {
private WeakReference<MainActivity> activityRef;
MyRunnable(MainActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
// 通过弱引用获取Activity
MainActivity activity = activityRef.get();
if (activity == null) {
return; // Activity已销毁,直接返回
}
// 执行耗时操作
}
}
}
避坑指南:我曾经在代码里看到过匿名内部类里直接更新UI的操作。如果Activity已经销毁,那就会抛出异常。所以用弱引用时,一定要判空。
2.3 Handler导致泄漏
Handler泄漏可以说是Android开发中最常见的泄漏之一。为什么?因为Handler在处理消息时,会持有外部类的引用。如果你在Activity里创建了一个Handler,发了个延迟消息,Activity退出了但消息还没处理,那Activity就泄漏了。
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
updateUI();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 发送一个延迟10分钟的消息
mHandler.sendEmptyMessageDelayed(0, 10 * 60 * 1000);
}
private void updateUI() {
// 更新界面
}
}
你想想看,用户打开页面,然后退出了。但这个Handler还在Looper的消息队列里挂着。Handler持有Activity引用,Activity就泄漏了。直到10分钟后消息被处理,或者Handler被移除,Activity才能被回收。
我记得有一次线上反馈说某个页面打开关闭几次就OOM了。一查,就是Handler搞的。那个页面有个定时刷新功能,Handler发了重复消息,用户快速进出,消息越积越多,Activity也越积越多。
修复方案有两个:
- 使用静态内部类+弱引用,跟匿名内部类的处理方式一样。
- 在onDestroy中移除所有消息和回调。
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.sendEmptyMessageDelayed(0, 10 * 60 * 1000);
}
@Override
protected void onDestroy() {
// 移除所有消息和回调
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityRef;
MyHandler(MainActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityRef.get();
if (activity == null) {
return;
}
activity.updateUI();
}
}
private void updateUI() {
// 更新界面
}
}
我的建议:如果你用Kotlin,可以考虑用协程替代Handler。协程的生命周期可以跟Activity绑定,自动取消,省心很多。但如果是维护老项目,还是老老实实把Handler的清理工作做好。
三种模式对比
| 泄漏模式 | 根本原因 | 常见场景 | 修复策略 |
|---|---|---|---|
| 静态变量持有Activity | 静态引用生命周期长于Activity | 工具类、单例模式中持有Context | 使用ApplicationContext或弱引用 |
| 匿名内部类隐式引用 | 内部类持有外部类引用 | 线程、回调、监听器 | 静态内部类+弱引用 |
| Handler导致泄漏 | 消息队列中的Message持有Handler | 延迟消息、定时任务 | 静态Handler+弱引用+onDestroy清理 |
这三种模式,说白了都是同一个问题:生命周期长的对象,持有了生命周期短的对象的引用。你只要记住这个核心原则,排查泄漏时就有了方向。下一章咱们聊聊怎么用工具把这些泄漏揪出来。