4. 常见内存泄漏场景(上):静态变量持有Activity、内部类隐式持有外部类、Handler导致的内存泄漏
好,咱们直接进入正题。内存泄漏这东西,说白了就是本该被回收的对象,因为被某个地方强引用着,GC拿它没办法。今天聊的三个场景,是我在项目里踩坑最多的。
4.1 静态变量持有Activity
先看一个最常见的坑。静态变量的生命周期跟应用进程一样长,而Activity的生命周期是跟着用户操作走的。一旦你用静态变量持有了Activity实例,那这个Activity就永远别想被回收了。
举个例子:
public class MainActivity extends AppCompatActivity {
private static Context sContext; // 静态持有Activity的Context
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sContext = this; // 这里就泄漏了
}
}
为什么会这样?因为sContext是静态的,它指向了MainActivity实例。当Activity被销毁时,sContext还死死拽着它。GC一看,嗯,还有人用着呢,不能回收。
我在项目中遇到过类似的情况。有一次线上反馈说用户连续打开关闭某个页面十几次后,应用直接OOM了。查了半天,发现是一个工具类里用静态变量缓存了Activity的Context。说白了,就是图方便,结果捅了篓子。
4.2 内部类隐式持有外部类
这个坑更隐蔽。Java里非静态内部类会隐式持有外部类的引用。你想想看,内部类对象活得比外部类长的时候,外部类就悲剧了。
看代码:
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
class MyTask implements Runnable {
@Override
public void run() {
// 这里隐式持有MainActivity.this
mTextView.setText("任务完成");
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new MyTask()).start();
}
}
MyTask是MainActivity的非静态内部类。它里面用到了mTextView,但实际上它持有的是整个MainActivity的引用。如果这个线程跑很久,Activity退出了,那MainActivity就泄漏了。
我曾经在一个图片处理模块里踩过这个坑。一个内部类AsyncTask在后台处理大图,用户等不及返回了,结果Activity一直没释放。嗯,那会儿内存占用直接飙到200多MB。
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
static class MyTask implements Runnable {
private WeakReference<MainActivity> mActivityRef;
MyTask(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void run() {
MainActivity activity = mActivityRef.get();
if (activity != null) {
activity.mTextView.setText("任务完成");
}
}
}
}
4.3 Handler导致的内存泄漏
Handler这个坑,我估计每个Android开发都遇到过。它跟内部类的问题类似,但更隐蔽,因为Handler往往跟Looper绑定,而Looper是线程私有的。
看这个经典写法:
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.sendMessageDelayed(Message.obtain(), 60000); // 延迟1分钟
}
}
问题出在哪?mHandler是匿名内部类,它持有MainActivity的引用。而Handler发送的消息会放在MessageQueue里,Message又持有Handler的引用。这一串引用链下来,Activity就泄漏了。
我记得有一次做直播应用,用户频繁进出直播间,结果内存一直涨。查了MAT分析,发现全是Handler和Message在作祟。说白了,就是延迟消息没清理。
正确的做法:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
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() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null); // 别忘了清理消息
}
}
小结一下
这三个场景,说白了都是同一个问题:生命周期长的对象持有了生命周期短的对象的引用。静态变量、内部类、Handler,只是表现形式不同而已。
| 泄漏场景 | 根本原因 | 推荐方案 |
|---|---|---|
| 静态变量持有Activity | 静态变量生命周期 = 应用进程 | 用Application Context,或在onDestroy置空 |
| 内部类隐式持有外部类 | 非静态内部类持有外部类引用 | 改成静态内部类 + 弱引用 |
| Handler泄漏 | Handler + MessageQueue引用链 | 静态内部类 + 弱引用 + onDestroy清理消息 |
嗯,这三个场景搞明白了,内存泄漏的问题就解决了一大半。下一节咱们继续聊剩下的常见场景,包括资源未关闭、监听器未解注册这些坑。