3. 匿名内部类隐式持有外部引用:Activity/Fragment泄漏根因

3.1 现象识别

当在 Activity 或 Fragment 中直接使用匿名内部类(或非静态内部类)作为 Retrofit Callback 时,即使界面已销毁,该 Activity/Fragment 实例仍无法被 GC 回收,导致内存泄漏。典型表现包括:

现象典型观测手段严重程度
退出页面后对象未被 GC 回收Android Studio Profiler → Dump Java Heap → 搜索 Activity 类名高
LeakCanary 提示 “ActivityName leaked”LeakCanary 通知栏 + Logcat中
频繁进入退出后 OOMLogcat 输出 OutOfMemoryError严重
反复横跳页面后 GC 耗时显著增加Profiler 内存图出现锯齿堆叠中
判断规则:在 Activity.onDestroy() 后,若其成员变量(如 TextView、RecyclerView)仍被其他线程持有引用,且该引用链中包含匿名内部类对象,则几乎可以确认发生匿名内部类泄漏。

3.2 数据采集

采集泄漏证据时,推荐使用 LeakCanary 自动捕获,或手动导出 Heap Dump 后用 MAT 分析。核心步骤:

  1. 启动 App,反复进入目标页面并返回,执行 3~5 次。
  2. 观察 LeakCanary 是否弹出泄漏通知。若无 LeakCanary,则在最后一次退出页面后约 5s 点击 Profiler → Dump Java Heap。
  3. 导出 .hprof 文件,用 Memory Analyzer (MAT) 打开。
  4. 使用 “List objects → with incoming references” 查询 Activity 实例,查看 GC Root 路径。

日志关键词检索:

# 使用 Android Studio Profiler 搜索
# 在 Dump 后的类列表里搜索 Activity 或 Fragment 的类名,如果看到多个实例且都“invisible”或“not needed”,则泄漏风险极高。

3.3 原因定位

3.3.1 引用链分析

非静态匿名内部类会隐式持有外部类的 this 引用。当 Retrofit 网络请求还未完成时,该回调对象生命周期长于 Activity,导致整个 Activity 无法被回收。

// 典型泄漏代码(错误示范)
public class MainActivity extends AppCompatActivity {
    private TextView mResultView;

    private void fetchData() {
        ApiService api = RetrofitClient.getInstance();
        Call call = api.getData();
        call.enqueue(new Callback<Data>() {      // 匿名内部类持有外部 Activity 引用
            @Override
            public void onResponse(Call<Data> call, Response<Data> response) {
                mResultView.setText(response.body().toString()); // 通过隐式引用操作 UI
            }
            @Override
            public void onFailure(Call<Data> call, Throwable t) { /* ... */ }
        });
    }
}

泄漏引用链:

  • GC Root → 网络线程池 → 正在执行的 Call → Retrofit Callback 匿名内部类 → 外部 Activity
匿名内部类泄漏引用链 GC Root 线程池 Retrofit Call 匿名内部类 Callback Activity / Fragment 关键:匿名内部类 obj 持有外部类 Activity.this 引用 即使 Activity 已 destroy,只要 Callback 仍在请求队列中,Activity 就无法被 GC 回收

3.3.2 故障树定位

是否满足条件结论
是匿名内部类中直接访问外部类的成员变量(如 TextView、List)隐式持有外部引用
是该回调在 Activity onDestroy 后仍被 Retrofit 线程池引用泄漏发生
是同时存在静态内部类或 lambda 替代后 GC 回收正常确认根因为非静态匿名内部类

3.4 修复动作

3.4.1 方案一:使用静态内部类 + 弱引用

public class MainActivity extends AppCompatActivity {
    private static class MyCallback implements Callback<Data> {
        private WeakReference<MainActivity> activityRef;

        MyCallback(MainActivity activity) {
            activityRef = new WeakReference<>(activity);
        }

        @Override
        public void onResponse(Call<Data> call, Response<Data> response) {
            MainActivity activity = activityRef.get();
            if (activity != null && !activity.isFinishing()) {
                activity.mResultView.setText(response.body().toString());
            }
        }

        @Override
        public void onFailure(Call<Data> call, Throwable t) { /* 同样处理 */ }
    }

    private TextView mResultView;

    private void fetchData() {
        ApiService api = RetrofitClient.getInstance();
        Call<Data> call = api.getData();
        call.enqueue(new MyCallback(this)); // 传入弱引用
    }
}

3.4.2 方案二:使用 Lambda(接口为函数式接口时适用)

如果 Retrofit Callback 定义是函数式接口(如 Callback<T> 在 Kotlin 中),可直接使用 Lambda,但注意 Lambda 在 Java 中仍可能捕获外部引用,建议配合 WeakReference 组合。

3.4.3 方案三:销毁时取消 Call

// 在 Activity 中维护 Call 引用
private Call<Data> mCall;

private void fetchData() {
    mCall = api.getData();
    mCall.enqueue(new Callback<Data>() { ... });
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mCall != null && !mCall.isCanceled()) {
        mCall.cancel(); // 取消请求,回调不再触发
    }
}
注意:仅取消 Call 并不一定能立即释放匿名内部类对 Activity 的引用,因为如果 Call 已在执行中,回调仍可能被调用。最佳实践是将方案一与方案三结合使用。

3.5 复测

修复后必须验证泄漏已被消除:

  • LeakCanary 验证:重复进入退出页面 5 次,确认无泄漏通知。
  • Profiler 内存观察:执行 GC 后,Activity 实例数归零,内存曲线平缓。
  • 自动化回归:集成 ActivityScenarioRule 在 Instrumentation 测试中检测泄漏(例如使用 LeakAssert)。

复测日志示例(无泄漏时):

D/LeakCanary: No leak detected.
D/ActivityManager: Process com.example.app (pid 12345) has no remaining instances of MainActivity after GC.

3.6 预防

预防措施说明
避免非静态匿名内部类做长时间回调改为静态内部类 + 弱引用,或使用 Lifecycle 感知组件
使用 ViewModel + LiveData将网络请求移至 ViewModel,回调返回后通过 LiveData 通知 UI
在 onDestroy 中取消所有 pending Call使用 CompositeDisposable 或直接取消 Call
代码审查规则禁止在 Activity/Fragment 的匿名内部类中直接访问成员变量
引入 LeakCanary 到 CI 流水线自动化检测泄漏,阻止合并泄漏代码
最佳实践:在 Kotlin 中优先使用协程 + LifecycleScope,天然防止泄漏;若使用 Java,则严格遵循静态内部类 + WeakReference 模式。