内存泄漏实战(上):常见泄漏场景
内存泄漏这个话题,说实话我每次做技术分享都会提到。为什么呢?因为太常见了。几乎每个项目里都能找到几个泄漏点,只是严重程度不同而已。今天咱们就聊聊三种最常见的泄漏场景——静态变量、内部类、Handler。这三种我都在线上项目里亲手排查过,有些坑踩得还挺深。
核心观点:内存泄漏的本质,就是本该被GC回收的对象,因为被某个长生命周期对象强引用着,导致无法释放。
静态变量持有Activity
先说说静态变量。这个场景其实很简单,但很多人会忽略。
你想想看,静态变量的生命周期是跟着ClassLoader走的,只要App进程活着,它就一直在。而Activity呢?它的生命周期是跟着用户操作走的,该销毁的时候就得销毁。如果静态变量里存了一个Activity引用,那这个Activity就永远别想被回收了。
我举个例子:
public class LeakManager {
private static Context sContext;
public static void init(Context context) {
sContext = context; // 这里传入了Activity
}
}
这段代码看着简单吧?但我在项目里见过类似的写法。有人图方便,把Activity当Application传进去了。结果呢?每次退出页面,Activity都泄漏了。
注意:静态变量持有Activity引用,是最容易避免的泄漏。只要记住一点——能用Application的地方,就别用Activity。
那正确的做法是什么?
- 如果确实需要Context,用Application.getApplicationContext()
- 如果必须持有Activity引用,记得在onDestroy里置空
- 考虑用弱引用(WeakReference)替代强引用
我个人习惯是,写工具类的时候,一律用ApplicationContext。除非这个工具类明确需要Activity级别的资源(比如弹Dialog),否则绝不传Activity进去。
内部类/匿名内部类隐式引用
这个场景就更有意思了。很多开发者不知道,非静态内部类会隐式持有外部类的引用。说白了,就是内部类对象里藏了一个指向外部类对象的指针。
为什么会这样?因为内部类要访问外部类的成员变量和方法,编译器就偷偷帮你加了个引用。你写代码的时候看不见,但内存里确实存在。
看个例子:
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
class MyTask implements Runnable {
@Override
public void run() {
// 这里隐式持有MainActivity的引用
mTextView.setText("任务完成");
}
}
private void startTask() {
new Thread(new MyTask()).start();
}
}
这段代码有什么问题?MyTask是非静态内部类,它隐式持有MainActivity的引用。如果这个线程执行时间很长,或者被某个全局的线程池管理着,那MainActivity就泄漏了。
我记得有一次排查线上OOM问题,就是这种场景。一个后台下载任务用了匿名内部类,用户快速退出重进页面,结果Activity泄漏了十几个实例。嗯,那场面挺壮观的。
避坑指南:我曾经在项目里定过一个规则——所有内部类都写成静态的。如果确实需要访问外部类,就显式传入一个WeakReference。这样代码虽然麻烦一点,但安全。
解决方案其实就几种:
- 把内部类改成静态内部类
- 用WeakReference显式持有外部类引用
- 在onDestroy里取消任务/置空引用
这里我多说一句。匿名内部类也是一样的道理。你写new Runnable(){}的时候,其实就是在创建一个匿名内部类,它同样会持有外部类的引用。所以,能用Lambda就用Lambda,Lambda不会持有外部类引用。
Handler与MessageQueue泄漏
这个场景可以说是Android开发者的"老朋友"了。几乎每个做过Android开发的人,都遇到过Handler泄漏的问题。
原理是这样的:Handler发送Message到MessageQueue,Message持有Handler的引用。如果Handler是非静态内部类,它就持有Activity的引用。MessageQueue是Looper管理的,Looper是线程局部变量,生命周期和线程一样长。主线程的Looper,那就是App的整个生命周期。
所以,一条链条就形成了:
MessageQueue → Message → Handler → Activity
只要MessageQueue里还有未处理的Message,Activity就泄漏了。
看代码:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
}
};
private void sendDelayMessage() {
mHandler.sendEmptyMessageDelayed(0, 60000); // 延迟1分钟
}
}
这段代码的问题很明显。如果用户在发送消息后立即退出Activity,这个Handler还在MessageQueue里挂着,Activity就泄漏了。
关键点:延迟消息是Handler泄漏的重灾区。因为延迟消息会在MessageQueue里停留一段时间,这段时间内Activity无法被回收。
那怎么解决?我一般用两种方式:
- 静态内部类 + WeakReference:把Handler写成静态内部类,用WeakReference持有Activity
- 在onDestroy里清理:调用handler.removeCallbacksAndMessages(null)移除所有消息
第一种方式更优雅,但代码量多一些。第二种方式简单粗暴,但容易遗漏。我个人习惯是两种都用——既用静态内部类,又在onDestroy里做清理。双重保险嘛。
public class MainActivity extends AppCompatActivity {
private static class SafeHandler extends Handler {
private final WeakReference<MainActivity> mActivityRef;
SafeHandler(MainActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivityRef.get();
if (activity == null) return;
// 处理消息
}
}
private SafeHandler mHandler = new SafeHandler(this);
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
这段代码就是标准写法了。注意看,SafeHandler是静态内部类,不会持有Activity引用。它通过WeakReference来获取Activity,如果Activity已经被回收了,就直接返回。
小技巧:如果你用的是Kotlin,可以用object关键字创建匿名对象,或者用Lambda表达式。Kotlin的Lambda不会持有外部类引用,比Java的匿名内部类安全得多。
知识体系总览
下面这张图,我把这三种泄漏场景的核心逻辑画出来了。你可以对照着看看,加深理解。
这三种场景,说白了都是同一个问题——生命周期不匹配。Activity的生命周期短,但被长生命周期的对象持有了。解决思路也很统一:要么切断引用链,要么用弱引用。
好了,这一章的内容就到这里。代码示例我都贴出来了,你可以直接拿去用。下一章咱们继续聊其他常见的泄漏场景,比如资源未关闭、监听器未解注册这些。到时候再细说。
公众号:蓝海资料掘金营,微信deep3321