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 | 中 |
| 频繁进入退出后 OOM | Logcat 输出 OutOfMemoryError | 严重 |
| 反复横跳页面后 GC 耗时显著增加 | Profiler 内存图出现锯齿堆叠 | 中 |
判断规则:在 Activity.onDestroy() 后,若其成员变量(如 TextView、RecyclerView)仍被其他线程持有引用,且该引用链中包含匿名内部类对象,则几乎可以确认发生匿名内部类泄漏。
3.2 数据采集
采集泄漏证据时,推荐使用 LeakCanary 自动捕获,或手动导出 Heap Dump 后用 MAT 分析。核心步骤:
- 启动 App,反复进入目标页面并返回,执行 3~5 次。
- 观察 LeakCanary 是否弹出泄漏通知。若无 LeakCanary,则在最后一次退出页面后约 5s 点击 Profiler → Dump Java Heap。
- 导出 .hprof 文件,用 Memory Analyzer (MAT) 打开。
- 使用 “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
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 模式。