内存泄漏实战(上):常见泄漏场景

内存泄漏这个话题,说实话我每次做技术分享都会提到。为什么呢?因为太常见了。几乎每个项目里都能找到几个泄漏点,只是严重程度不同而已。今天咱们就聊聊三种最常见的泄漏场景——静态变量、内部类、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无法被回收。

那怎么解决?我一般用两种方式:

  1. 静态内部类 + WeakReference:把Handler写成静态内部类,用WeakReference持有Activity
  2. 在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 静态变量 → Activity引用 生命周期:App全程 Activity无法被回收 内部类隐式引用 内部类 → 外部类引用 编译器自动添加 线程/任务未结束则泄漏 Handler + MessageQueue MessageQueue → Message → Handler → Activity 延迟消息期间泄漏 ✅ 解决方案 ① 静态变量 → 使用ApplicationContext,或在onDestroy置空 ② 内部类 → 改为静态内部类 + WeakReference ③ Handler → 静态内部类 + WeakReference + onDestroy清理消息 核心原则:短生命周期对象不要被长生命周期对象强引用

这三种场景,说白了都是同一个问题——生命周期不匹配。Activity的生命周期短,但被长生命周期的对象持有了。解决思路也很统一:要么切断引用链,要么用弱引用。

好了,这一章的内容就到这里。代码示例我都贴出来了,你可以直接拿去用。下一章咱们继续聊其他常见的泄漏场景,比如资源未关闭、监听器未解注册这些。到时候再细说。


公众号:蓝海资料掘金营,微信deep3321