2. 内存泄漏四大常见场景:静态变量、内部类、Handler、匿名内部类

做性能优化这么多年,我见过最多的 ANR,其实根源都在内存泄漏上。今天咱们就挑四个最常见的“坑”来讲——静态变量、非静态内部类、Handler、匿名内部类。这几个家伙表面上不显眼,但一旦不小心,它们就会偷偷抱着 Activity 的引用不撒手,GC 收不掉,内存越堆越高,系统一看不对劲,直接给你个 ANR。

常见内存泄漏场景 内存泄漏 静态变量 非静态内部类 Handler 匿名内部类 核心问题:这些场景都持有了 Activity / Context 的引用,Activity 无法被 GC 回收

2.1 静态变量——最经典的“钉子户”

静态变量属于类,不随 Activity 销毁而释放。如果你用静态变量直接或间接引用了 Activity,那这个 Activity 就成了“钉子户”,永远待在内存里。我个人习惯写工具类时,绝不把 Activity 的 Context 塞进静态变量,哪怕只是暂时存一下。

举个最简单的例子:

public class Global {
    public static Activity sActivity; // 危险!
}

我曾经接手过一个项目,启动页崩得特别厉害。查了半天,发现是某个同事在 Application 里搞了个静态 ArrayList,把 Activity 对象全丢进去了。用户点返回键根本清不掉,堆叠越来越多,最后 ANR。解决方案?要么用 WeakReference,要么把数据移出静态区。

避坑指南:

  • 能用局部变量就别用静态变量
  • 实在要存 Context,用 Application Context,别用 Activity Context
  • 可以考虑 WeakReference 来持有 Activity 对象

2.2 非静态内部类——藏着隐式引用

非静态内部类(比如在 Activity 内部定义的类)会自动持有外部类的引用。你想想看,很多时候我们为了方便,直接在 Activity 里定义一堆内部类,比如 ViewHolder、Listener,甚至网络回调。这些内部类一旦被其他地方引用,外部 Activity 就被连带保住了。

我自己的经验是:只要内部类不需要访问外部类的实例成员,就改成静态内部类。如果非要访问,那就用 WeakReference 方式引用外部类,或者干脆抽成独立类。

// 错误写法
public class MyActivity extends AppCompatActivity {
    class TaskRunnable implements Runnable {
        @Override
        public void run() {
            // 这里隐式持有 MyActivity.this
        }
    }
}

// 正确写法
public class MyActivity extends AppCompatActivity {
    static class TaskRunnable implements Runnable {
        private WeakReference<MyActivity> activityRef;
        TaskRunnable(MyActivity activity) {
            activityRef = new WeakReference<>(activity);
        }
        @Override
        public void run() {
            MyActivity activity = activityRef.get();
            if (activity != null) {
                // 安全使用
            }
        }
    }
}

注意:有些同事觉得“我就用一次,应该没事”。但异步任务、网络回调的生命周期往往比 Activity 长,等你回来 Activity 早该销毁了。这锅还是得静态内部类来背。

2.3 Handler——消息队列里的定时炸弹

Handler 的内部实现原理导致它必须持有 Looper 的对象消息队列。如果你在 Activity 里直接 new Handler(),这个 Handler 就会持有外部 Activity 的引用。当消息还没处理完(比如延时任务),Activity 被销毁了,Handler 依然在消息队列里,导致 Activity 泄漏。

我记得有一次做直播间功能,用户退出直播间后,内存一直涨。用 Memory Profiler 一查,发现有个 Handler 发了个 30 秒后的定时任务,用户早就切出去了,那个 Activity 一直没被回收。修复方式很简单——

  • 使用静态内部类 + WeakReference
  • 在 Activity 的 onDestroy() 里调用 handler.removeCallbacksAndMessages(null) 清除所有消息
public class MainActivity extends AppCompatActivity {
    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;
        MyHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();
            if (activity != null) {
                // 处理消息
            }
        }
    }
    private MyHandler mHandler = new MyHandler(this);
    
    @Override
    protected void onDestroy() {
        mHandler.removeCallbacksAndMessages(null); // 必加!
        super.onDestroy();
    }
}

小技巧:如果你用 Kotlin 开发,可以直接用 Handler(Looper.getMainLooper()) 创建的 Handler 不会持有 Activity 吗?其实也会——因为 Handler 本身没有强引用 Activity,但 message 的 callback 如果是匿名内部类或 lambda,还是会有引用。所以最好的方案还是清除消息队列。

2.4 匿名内部类——语法糖背后的陷阱

匿名内部类本质上跟非静态内部类一样,也持有外部类的引用。比如常见的 View.OnClickListener,在 Activity 里通过 new 接口的方式创建,就相当于定义了一个匿名内部类,它强制持有了 Activity。不过那种短生命周期的回调还好——因为通常在方法内部用完就释放了。真正的威胁是把匿名内部类对象赋给了静态变量,或者传给了生命周期更长的对象。

我曾经排查过一个复杂的 bug:用户反复横滑页面,内存暴涨。最后发现有一个 ViewPager 的 onPageChangeListener 是匿名内部类实现,而且这个 Listener 被注册到了 Application 级别的某个单例里。结果每次横滑都创建一个新的匿名内部类,旧的又无法释放,因为被单例引用着。嗯,这种场景下,匿名内部类就是泄漏元凶。

泄漏场景根本原因推荐做法
静态变量持有 ActivityGC 无法回收静态引用的对象用 WeakReference 或 Application Context
非静态内部类隐式持有外部类引用改为静态内部类 + WeakReference
Handler 消息未清除消息队列保留 Handler 的引用onDestroy 时 removeCallbacksAndMessages
匿名内部类被长生命周期持有内部类隐式引用外部类避免赋给静态变量,或改成静态内部类

四个场景其实都指向同一个问题——对象生命周期错配。Activity 的生命周期很短,但引用它的对象却活得很长。你只要记住:任何能活过 Activity 的对象,都不要直接持有 Activity 的强引用。多用 WeakReference,多习惯在 onDestroy 里清理资源,ANR 就会离你远一点。