2、常见泄漏模式:静态变量持有Activity、匿名内部类隐式引用、Handler导致泄漏

内存泄漏这东西,说白了就是本该被回收的对象,因为被不该持有它的对象拽着,导致GC没法处理。我这些年排查过的泄漏案例,十有八九都逃不出这三种模式。今天咱们就把它们掰开揉碎了讲清楚。

2.1 静态变量持有Activity

先看一个最经典的场景。你写了个工具类,里面放了个静态变量,不小心把Activity传进去了。嗯,这就是泄漏的起点。

public class LeakExample {
    private static Context sContext;
    
    public static void init(Context context) {
        sContext = context;  // 这里传入了Activity
    }
}

为什么会泄漏?静态变量的生命周期跟Application一样长。Activity退出后,这个静态引用还死死拽着它。GC一看,有人引用着呢,不能回收。于是Activity就永远躺在内存里了。

我在项目中遇到过类似情况。有个同事写了个图片加载工具,为了方便,直接把Activity的Context存成了静态变量。结果用户反复进出某个页面,内存蹭蹭往上涨。最后用MAT一分析,好家伙,十几个Activity实例全挂着。

核心原则:静态变量不要直接或间接持有Activity、Fragment、View等生命周期短的对象。如果非要持有Context,请用ApplicationContext。

修复方案其实很简单:

public class SafeExample {
    private static Context sContext;
    
    public static void init(Context context) {
        // 只保留ApplicationContext
        sContext = context.getApplicationContext();
    }
    
    // 或者干脆不用静态变量,用弱引用
    private static WeakReference<Context> sContextRef;
    
    public static void initSafe(Context context) {
        sContextRef = new WeakReference<>(context.getApplicationContext());
    }
}

我的习惯:能用局部变量就别用静态变量。实在要用,记得在不需要时置为null。比如在Activity的onDestroy里调用LeakExample.release()。

2.2 匿名内部类隐式引用

这个坑我踩过不止一次。匿名内部类看起来方便,但它会隐式持有外部类的引用。你想想看,一个匿名Runnable被丢到线程池里执行,线程跑完了才能释放。如果这个Runnable是在Activity里创建的,那Activity就得一直等着。

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 匿名内部类,隐式持有MainActivity.this
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模拟耗时操作
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

用户按返回键退出了Activity,但这个线程还在跑。线程里的Runnable对象持有Activity引用,Activity就没法回收。10秒后线程结束,Runnable被释放,Activity才能被GC处理。这10秒里,Activity就白白占着内存。

我曾经在一个社交App里排查过类似问题。用户发帖时上传图片,后台开了个线程做压缩。结果用户快速退出重进,Activity实例越积越多。最后定位到就是匿名内部类搞的鬼。

修复方案:用静态内部类+弱引用。

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 使用静态内部类
        new Thread(new MyRunnable(this)).start();
    }
    
    private static class MyRunnable implements Runnable {
        private WeakReference<MainActivity> activityRef;
        
        MyRunnable(MainActivity activity) {
            this.activityRef = new WeakReference<>(activity);
        }
        
        @Override
        public void run() {
            // 通过弱引用获取Activity
            MainActivity activity = activityRef.get();
            if (activity == null) {
                return; // Activity已销毁,直接返回
            }
            // 执行耗时操作
        }
    }
}

避坑指南:我曾经在代码里看到过匿名内部类里直接更新UI的操作。如果Activity已经销毁,那就会抛出异常。所以用弱引用时,一定要判空。

2.3 Handler导致泄漏

Handler泄漏可以说是Android开发中最常见的泄漏之一。为什么?因为Handler在处理消息时,会持有外部类的引用。如果你在Activity里创建了一个Handler,发了个延迟消息,Activity退出了但消息还没处理,那Activity就泄漏了。

public class MainActivity extends AppCompatActivity {
    
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 处理消息
            updateUI();
        }
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 发送一个延迟10分钟的消息
        mHandler.sendEmptyMessageDelayed(0, 10 * 60 * 1000);
    }
    
    private void updateUI() {
        // 更新界面
    }
}

你想想看,用户打开页面,然后退出了。但这个Handler还在Looper的消息队列里挂着。Handler持有Activity引用,Activity就泄漏了。直到10分钟后消息被处理,或者Handler被移除,Activity才能被回收。

我记得有一次线上反馈说某个页面打开关闭几次就OOM了。一查,就是Handler搞的。那个页面有个定时刷新功能,Handler发了重复消息,用户快速进出,消息越积越多,Activity也越积越多。

修复方案有两个:

  1. 使用静态内部类+弱引用,跟匿名内部类的处理方式一样。
  2. 在onDestroy中移除所有消息和回调
public class MainActivity extends AppCompatActivity {
    
    private MyHandler mHandler = new MyHandler(this);
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.sendEmptyMessageDelayed(0, 10 * 60 * 1000);
    }
    
    @Override
    protected void onDestroy() {
        // 移除所有消息和回调
        mHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
    
    private static class MyHandler extends Handler {
        private WeakReference<MainActivity> activityRef;
        
        MyHandler(MainActivity activity) {
            this.activityRef = new WeakReference<>(activity);
        }
        
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityRef.get();
            if (activity == null) {
                return;
            }
            activity.updateUI();
        }
    }
    
    private void updateUI() {
        // 更新界面
    }
}

我的建议:如果你用Kotlin,可以考虑用协程替代Handler。协程的生命周期可以跟Activity绑定,自动取消,省心很多。但如果是维护老项目,还是老老实实把Handler的清理工作做好。

三种模式对比

泄漏模式 根本原因 常见场景 修复策略
静态变量持有Activity 静态引用生命周期长于Activity 工具类、单例模式中持有Context 使用ApplicationContext或弱引用
匿名内部类隐式引用 内部类持有外部类引用 线程、回调、监听器 静态内部类+弱引用
Handler导致泄漏 消息队列中的Message持有Handler 延迟消息、定时任务 静态Handler+弱引用+onDestroy清理

这三种模式,说白了都是同一个问题:生命周期长的对象,持有了生命周期短的对象的引用。你只要记住这个核心原则,排查泄漏时就有了方向。下一章咱们聊聊怎么用工具把这些泄漏揪出来。