3、常见内存泄漏场景:静态变量持有Activity、Handler导致的内存泄漏、匿名内部类持有外部引用
内存泄漏这个话题,说实话我每次做性能优化时都会碰到。它不像崩溃那样直接,但日积月累下来,App会越来越卡,甚至被系统直接杀掉。今天咱们就聊聊三个最常见的泄漏场景,都是我亲手踩过的坑。
3.1 静态变量持有Activity引用
先看一个最简单的例子:
public class LeakActivity extends AppCompatActivity {
private static Context sContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 这行代码就是泄漏的根源
sContext = this;
}
}
为什么这样会泄漏?静态变量的生命周期和Application一样长,而Activity的生命周期是短暂的。当Activity被销毁后,静态变量还死死拽着它,GC根本没法回收。
我在项目中遇到过类似的情况。有一次线上反馈说用户连续打开关闭某个页面十几次后,App就卡得不行。用MAT一分析,好家伙,一个静态的Bitmap引用把整个Activity链都hold住了。那个Bitmap其实是个启动页的背景图,本意是想复用,结果成了内存杀手。
我曾经接手过一个项目,里面有个全局的「数据缓存中心」,全部用静态变量存着Activity的引用。结果就是用户每打开一个页面,内存就涨一块,永远不降。这种设计说白了就是定时炸弹。
正确的做法是什么?
- 能用ApplicationContext就别用Activity的Context
- 静态变量只存Application级别的数据
- 如果一定要存Activity引用,记得在onDestroy里置空
3.2 Handler导致的内存泄漏
这个场景太经典了,几乎每个Android开发者都遇到过。来看代码:
public class HandlerLeakActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
updateUI();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 发送一个延迟消息
mHandler.sendMessageDelayed(Message.obtain(), 60000);
}
}
问题出在哪?Handler在发送延迟消息时,这个消息会持有Handler的引用。而Handler作为匿名内部类,又隐式持有外部Activity的引用。如果Activity在消息还没处理完就被销毁了,整个引用链就断不开了。
你想想看,用户打开页面,发了个延迟60秒的消息,然后立刻关掉页面。按理说Activity应该被回收,但因为消息队列里还躺着一条消息,整个Activity就被强行续命了60秒。如果用户频繁进出,内存就会像滚雪球一样越滚越大。
我个人习惯用静态内部类+弱引用的方式来解决:
private static class SafeHandler extends Handler {
private WeakReference<HandlerLeakActivity> mActivityRef;
SafeHandler(HandlerLeakActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
HandlerLeakActivity activity = mActivityRef.get();
if (activity != null) {
activity.updateUI();
}
}
}
另外,别忘了在onDestroy里清理消息:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
3.3 匿名内部类持有外部引用
这个场景其实和Handler的原理类似,但表现形式更多样。比如:
public class InnerClassLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 匿名内部类,隐式持有Activity引用
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 这里可以访问Activity的成员变量
doSomething();
}
});
// 或者这样
new Thread(new Runnable() {
@Override
public void run() {
// 长时间运行的任务
while (true) {
// 持有Activity引用,导致无法回收
}
}
}).start();
}
}
说白了,匿名内部类天生就会持有外部类的引用。这不是bug,是Java语言的设计。但如果我们不注意,这个特性就会变成内存泄漏的温床。
我记得有一次排查一个内存泄漏问题,发现是某个Fragment里启动了一个无限循环的线程,线程里用匿名内部类的方式回调更新UI。结果Fragment被销毁后,那个线程还在跑,Fragment就永远活在内存里了。
| 场景 | 泄漏原因 | 解决方案 |
|---|---|---|
| 静态变量 | 生命周期不匹配 | 用ApplicationContext或及时置空 |
| Handler | 延迟消息持有引用 | 静态内部类+弱引用+清理消息 |
| 匿名内部类 | 隐式持有外部引用 | 改为静态内部类或及时停止任务 |
嗯,这三个场景其实都有一个共同点:长生命周期对象持有了短生命周期对象的引用。理解了这一点,你就能举一反三,发现更多潜在的泄漏点。
最后说一句,内存泄漏的排查工具我推荐用LeakCanary,它能在泄漏发生的第一时间通知你。但工具只是辅助,真正要避免泄漏,还是得靠我们对这些场景的深刻理解。