4. 常见内存泄漏场景(上):静态变量持有Activity、内部类隐式持有外部类、Handler导致的内存泄漏

好,咱们直接进入正题。内存泄漏这东西,说白了就是本该被回收的对象,因为被某个地方强引用着,GC拿它没办法。今天聊的三个场景,是我在项目里踩坑最多的。

4.1 静态变量持有Activity

先看一个最常见的坑。静态变量的生命周期跟应用进程一样长,而Activity的生命周期是跟着用户操作走的。一旦你用静态变量持有了Activity实例,那这个Activity就永远别想被回收了。

⚠️ 警告: 这是新手最容易犯的错误,也是代码Review时我必查的点。

举个例子:

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。说白了,就是图方便,结果捅了篓子。

💡 我的建议: 能用Application Context就别用Activity Context。如果非要传Activity,记得在onDestroy里把静态变量置空。

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在作祟。说白了,就是延迟消息没清理。

⚠️ 避坑指南: 我曾经在onDestroy里只调了removeCallbacksAndMessages(null),但发现还是泄漏。后来才意识到,如果Handler是匿名内部类,即使清空消息,Handler本身还持有Activity引用。所以必须用静态内部类。

正确的做法:

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清理消息

嗯,这三个场景搞明白了,内存泄漏的问题就解决了一大半。下一节咱们继续聊剩下的常见场景,包括资源未关闭、监听器未解注册这些坑。