常见泄漏场景:静态变量持有Activity、内部类隐式持有外部类、Handler导致泄漏
好,咱们直接进入正题。内存泄漏这东西,说白了就是本该被回收的对象,因为被某个“钉子户”强引用着,GC拿它没办法。我排查过上百个泄漏案例,发现80%以上都逃不出今天讲的这三个坑。
场景一:静态变量持有Activity
先看一个我印象特别深的例子。有一次线上反馈,用户反复进入某个详情页后,App直接OOM崩溃。我抓了heap dump一看,Activity实例竟然有几十个,全被一个静态的ArrayList拽着。
为什么会这样?
public class DataManager {
private static List<Listener> sListeners = new ArrayList<>();
public static void registerListener(Listener listener) {
sListeners.add(listener);
}
}
// 在Activity中注册
public class MainActivity extends AppCompatActivity implements DataManager.Listener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DataManager.registerListener(this); // 问题就在这里
}
}
你看,sListeners是静态的,生命周期和App一样长。Activity把自己注册进去后,即使退出了,静态列表还持有它的引用。GC一看,嗯,还有人用着,不能回收。
核心要点:静态变量的生命周期 = 进程生命周期。任何传入静态集合的Activity引用,都必须显式移除。
我个人习惯的做法是:
- 在
onDestroy()里调用DataManager.unregisterListener(this) - 或者改用弱引用(WeakReference)包装Activity
- 再或者,干脆用EventBus这类解耦框架
避坑指南:我曾经接手过一个项目,里面有个全局的静态HashMap缓存了所有Activity的引用,美其名曰“方便管理”。结果每个Activity泄漏几十KB,用户切几个页面就炸了。记住:静态变量只适合存Application Context或者纯数据,千万别存Activity。
场景二:内部类隐式持有外部类
这个坑更隐蔽。Java内部类会隐式持有外部类的引用,你写代码时根本看不见那个this$0字段。
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 匿名内部类,隐式持有MainActivity.this
new Thread(new Runnable() {
@Override
public void run() {
// 模拟耗时操作
try { Thread.sleep(10000); } catch (InterruptedException e) { }
// 这里访问了外部类的成员变量
mTextView.setText("完成");
}
}).start();
}
}
你想想看,这个Runnable是个匿名内部类,它偷偷持有了MainActivity的引用。如果Activity在10秒内退出了,这个线程还在跑,那Activity就泄漏了。
| 内部类类型 | 是否隐式持有外部类 | 泄漏风险 |
|---|---|---|
| 非静态内部类 | 是 | 高 |
| 匿名内部类 | 是 | 高 |
| 静态内部类 | 否 | 低 |
| 局部内部类 | 是(如果访问外部成员) | 中 |
怎么解决?我建议你养成一个习惯:
- 能用静态内部类就别用非静态的
- 如果必须用内部类,把外部引用用WeakReference包一层
- 异步任务结束后,记得清理回调
注意:即使内部类没有显式访问外部类的成员变量,它仍然持有外部类的引用。这是Java语言规范决定的,你躲不掉。唯一的办法就是改成静态内部类。
场景三:Handler导致泄漏
这个可以说是Android开发者的“老朋友”了。每个写过Handler的人,大概率都踩过这个坑。
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息,这里隐式持有MainActivity
updateUI();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 发送一个延迟消息
mHandler.sendEmptyMessageDelayed(0, 60000); // 1分钟后执行
}
}
问题出在哪?
- Handler是匿名内部类,持有外部Activity的引用
- 发送的消息Message又持有Handler的引用(通过target字段)
- Message被放在MessageQueue里,MessageQueue被Looper持有
- Looper是线程局部变量,生命周期和线程一样长
这一串引用链下来,Activity就被牢牢拴住了。哪怕用户已经退出了,只要消息还没处理,Activity就泄漏着。
引用链:Looper → MessageQueue → Message → Handler → Activity
我在项目中遇到过类似情况,当时是一个聊天页面,用Handler做心跳包。用户切到后台后,Activity一直泄漏,直到OOM。后来我用了两种方案:
方案一:静态内部类 + 弱引用
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<MainActivity> mActivityRef;
MyHandler(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivityRef.get();
if (activity != null) {
activity.updateUI();
}
}
}
@Override
protected void onDestroy() {
// 关键:移除所有未处理的消息和回调
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
方案二:使用Lifecycle感知组件
现在有了AndroidX,我推荐直接用LiveData或者协程。它们能自动感知生命周期,省心很多。
我的经验:在onDestroy()里调用removeCallbacksAndMessages(null)是个好习惯。但要注意,如果Handler是静态的,记得在不需要时把Handler置空,否则弱引用也救不了你。
总结一下这三个场景
| 泄漏场景 | 根本原因 | 推荐解法 |
|---|---|---|
| 静态变量持有Activity | 静态变量生命周期 > Activity | 及时移除引用 / 用WeakReference |
| 内部类隐式持有外部类 | 非静态内部类持有外部类引用 | 改成静态内部类 / 弱引用包装 |
| Handler导致泄漏 | 消息队列持有Handler链 | 静态Handler + 弱引用 + 清理消息 |
嗯,这三个场景你排查时优先看。我一般拿到heap dump后,先搜Activity的实例数,如果发现本该销毁的Activity还活着,就顺着GC Root链往上查。八成都能落到这三个坑里。
下一章咱们聊聊更隐蔽的泄漏场景——比如资源未关闭、监听器未解注册这些。到时候我会分享一个我花了三天才定位到的奇葩泄漏案例。