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,它能在泄漏发生的第一时间通知你。但工具只是辅助,真正要避免泄漏,还是得靠我们对这些场景的深刻理解。