4、静态变量泄漏:静态Activity Context导致的内存泄漏场景与修复

静态变量泄漏,这可以说是Android内存泄漏里的“老熟人”了。我入行那会儿,第一个被review打回来的代码,就是因为这个。

说白了,就是你把一个本该被回收的对象,用static关键字给“钉死”在了内存里。Java的垃圾回收器再厉害,也动不了静态变量引用的对象。你想想看,一个Activity本该在退出时被销毁,结果因为某个静态变量还拽着它,它就只能在内存里干瞪眼。

4.1 典型的泄漏场景:静态Context

最常见的场景,就是在一个工具类或者Application类里,定义了一个静态的Context变量。比如这样:

public class Utils {
    private static Context sContext;

    public static void init(Context context) {
        sContext = context;
    }
}

然后你在某个Activity里调用了Utils.init(this)。嗯,问题来了。这个this是Activity的实例,它被静态变量sContext持有了。只要App进程活着,这个Activity就永远无法被回收。

我在项目中遇到过类似的情况。一个同事为了图方便,把Activity的Context传给了静态变量,结果那个Activity的onDestroy()从来没被调用过。页面退出了,但内存里的对象还在,甚至还在偷偷执行一些任务。最后导致了一系列奇怪的崩溃。

⚠️ 警告: 静态变量持有Activity Context,是内存泄漏的“头号通缉犯”。一旦发生,泄漏的对象可能包括整个View树、Drawable、以及其他关联资源。一个Activity泄漏,可能带走几MB甚至十几MB的内存。

4.2 为什么会泄漏?

我们来拆解一下泄漏的链条:

  1. 静态变量属于类,生命周期与App进程一致。
  2. Activity Context内部持有Activity的引用。
  3. 静态变量 → Activity Context → Activity → 整个视图层级。

这个链条一旦建立,就是一条“不归路”。GC(垃圾回收)从根节点(GC Roots)开始遍历,静态变量就是根节点之一。只要它还在,整个链条上的对象都不会被标记为垃圾。

你可能会问:“那我用getApplicationContext()不就行了?”没错,这正是修复的关键。

4.3 修复方案:用Application Context代替

修复其实很简单。把静态变量里存的Context,换成Application级别的Context。因为Application的生命周期和进程一样长,不存在泄漏问题。

public class Utils {
    private static Context sContext;

    public static void init(Context context) {
        // 关键:只取Application Context
        sContext = context.getApplicationContext();
    }
}

或者更干脆一点,在构造时就强制要求传入Application Context:

public class Utils {
    private static Context sContext;

    public static void init(Application application) {
        sContext = application;
    }
}

我个人习惯在工具类的init()方法里,直接调用context.getApplicationContext()。这样不管调用方传什么Context进来,最终存的都是安全的Application实例。

💡 小技巧: 如果你不确定某个Context是不是Activity实例,可以用 instanceof 判断一下。但更好的做法是,从一开始就只接受Application类型的参数。

4.4 其他常见的静态泄漏场景

除了静态Context,还有几个“坑”我经常见到:

场景 泄漏原因 修复方法
静态View 静态变量持有Activity的某个View 使用弱引用,或在Activity销毁时置空
静态内部类 静态内部类隐式持有外部类引用 改为非静态内部类,或使用弱引用
单例模式 单例对象持有了Activity的引用 单例中只使用Application Context
静态集合 静态List或Map中存了Activity对象 及时移除,或使用WeakHashMap

我曾经接手过一个项目,里面有个静态的ArrayList<Activity>,用来管理所有打开的Activity。初衷是为了方便“一键关闭所有页面”。结果呢?每个Activity都被这个静态列表拽着,一个都释放不了。页面堆叠多了,内存直接飙到300MB以上。

修复方案其实也简单:要么用WeakReference包装一下,要么在Activity的onDestroy()里主动从列表中移除。我个人更推荐后者,因为弱引用虽然能解决泄漏,但对象被回收的时机不可控,可能带来其他问题。

4.5 避坑指南

这里我总结几条实战经验,希望能帮你少走弯路:

  • 静态变量里不要存Activity、Fragment、View。这是铁律。
  • 单例对象里如果需要Context,只存Application Context。在getInstance()方法里强制转换一下。
  • 静态集合要小心。如果必须用,记得在合适的时机清理。比如在onDestroy()里调用remove()
  • 匿名内部类+静态变量。匿名内部类会持有外部类的引用,如果这个匿名内部类被静态变量引用,外部类也会泄漏。
🔍 排查技巧: 用Android Studio的Profiler工具,抓取内存快照(Heap Dump)。然后搜索你的Activity类名,看看有没有多余的实例。如果有,顺着GC Root路径找上去,十有八九能看到静态变量的身影。

嗯,这里还要提一句。有些同学觉得“我App很小,泄漏一点没关系”。千万别这么想。内存泄漏就像慢性病,初期没症状,等用户手机卡顿、闪退的时候,往往已经积重难返了。我见过太多因为静态泄漏导致OOM(内存溢出)的案例,修复起来其实就几行代码的事,但排查过程可能要花上几天。

所以,写代码的时候多留个心眼。看到static关键字,就问问自己:这个变量真的需要静态吗?它引用的对象生命周期合适吗?养成这个习惯,能帮你避免90%的静态泄漏问题。