常见泄漏原因:Activity 中匿名内部类 Observer 未取消注册、Fragment 生命周期与 View 分离导致
4.1 现象识别
两种典型泄漏场景在 Android 内存分析中呈现不同的表象。通过识别以下特征,可以快速缩小排查范围。
| 对比维度 | 场景 A:Activity 匿名内部类 | 场景 B:Fragment View 分离 |
|---|---|---|
| 泄漏对象 | Activity 实例 | Fragment 及其 View 实例 |
| 触发时机 | Activity 执行 finish() 后无法 GC | Fragment 进入回退栈,View 被销毁后无法 GC |
| 关键 API 误用 | observeForever() 未配对 removeObserver() |
使用 getActivity() 作为 LifecycleOwner |
| 堆栈特征 | Observer 匿名内部类 → 持有 Activity.this | Observer 匿名内部类 → 持有 Fragment/View |
| 泄漏速度 | 每次 finish 累积一个 Activity | 每次 View 分离累积一个 Fragment 链 |
快速判断规则:
如果 Heap 中目标 Activity 实例数 > 1 且 GC Root 路径包含
如果 Fragment 实例存活但其
如果 Heap 中目标 Activity 实例数 > 1 且 GC Root 路径包含
Observer,优先归入场景 A;如果 Fragment 实例存活但其
mView 为空且被 Observer 引用,优先归入场景 B。
4.2 数据采集
使用 Android Studio Memory Profiler 进行现场取证,捕获两种场景的关键证据。
- 复现操作:反复进入并退出目标 Activity / 反复 add+pop 目标 Fragment 至回退栈。
- GC 触发:在 Profiler 中手动点击 "Force Java GC",观察内存曲线是否回到基线。
- Heap Dump:若内存未回落,立即 Dump Heap。
- 实例搜索:在 Heap Viewer 中搜索 Activity 或 Fragment 类名,记录存活实例数。
- GC Root 分析:选中多余实例,查看
Path to GC Root,确认是否通过 Observer 匿名内部类可达。
关键证据清单:
- 目标类实例数 > 预期存活数(通常应为 1 或 0)
- GC Root 路径中包含
LiveData或Observer节点 - Observer 内部类持有外部类引用(
this$0字段)
4.3 原因定位 —— 故障树分析
下面的故障树从 "LiveData Observer 注册" 出发,分两条路径直达泄漏根源,帮助工程师快速判定具体场景。
4.4 修复动作
根据故障树定位的具体场景,采取对应的代码修复方案。
场景 A:Activity 匿名内部类修复
问题代码:使用 observeForever() 但未在 onDestroy() 中移除 Observer,导致匿名内部类持有 Activity 引用。
// 问题写法:observeForever 未配对 removeObserver
public class MainActivity extends AppCompatActivity {
private LiveData<String> liveData;
private Observer<String> observer; // 需要持有引用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
liveData = ...;
observer = new Observer<String>() {
@Override
public void onChanged(String s) {
// 匿名内部类隐式持有 Activity.this
textView.setText(s);
}
};
liveData.observeForever(observer);
// 缺少:在 onDestroy 中移除
}
@Override
protected void onDestroy() {
super.onDestroy();
// 修复:显式移除 Observer
liveData.removeObserver(observer);
}
}
场景 B:Fragment View 分离修复
问题代码:使用 getActivity() 作为 LifecycleOwner,导致 Observer 生命周期与 Activity 绑定而非与 Fragment View 绑定。
// 问题写法:getActivity() 作为 LifecycleOwner
public class MyFragment extends Fragment {
private LiveData<String> liveData;
private TextView textView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
textView = view.findViewById(R.id.text_view);
liveData = ...;
// 问题:使用 getActivity() 作为 LifecycleOwner
liveData.observe(getActivity(), new Observer<String>() {
@Override
public void onChanged(String s) {
// 匿名内部类持有 Fragment 引用(通过 textView)
textView.setText(s);
}
});
return view;
}
// Fragment 进入回退栈 → View 销毁 → Observer 仍存活 → 泄漏
}
// 修复写法:使用 getViewLifecycleOwner()
public class MyFragment extends Fragment {
private LiveData<String> liveData;
private TextView textView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, container, false);
textView = view.findViewById(R.id.text_view);
liveData = ...;
// 修复:使用 getViewLifecycleOwner()
liveData.observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onChanged(String s) {
// View 销毁时自动移除 Observer
textView.setText(s);
}
});
return view;
}
}
⚠ 修复要点:
- 场景 A 必须保证
removeObserver()与observeForever()成对出现,推荐在onDestroy()中调用。 - 场景 B 优先使用
getViewLifecycleOwner();若需在 Fragment 全局范围内观察,使用Lifecycle.Event.ON_DESTROY手动移除。 - 匿名内部类始终持有外部类引用,无法避免时改用静态内部类 + WeakReference。
4.5 复测验证
修复后必须通过以下步骤确认泄漏已消除。
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 安装修复版 APK,打开目标页面 | 页面正常显示,无异常 |
| 2 | 反复进入并退出 Activity / 反复 add+pop Fragment | 操作流畅,无卡顿 |
| 3 | Memory Profiler 中触发 Force GC | 内存曲线回到基线 |
| 4 | Dump Heap 并搜索目标类实例数 | Activity / Fragment 实例数 ≤ 1 |
| 5 | 检查 GC Root 路径 | 不再出现 Observer 引用链 |
验证判断规则:
若 Dump Heap 中目标类实例数 > 1,且 Path to GC Root 仍包含 Observer → 修复未生效,返回 4.4 重新检查代码;
若实例数 ≤ 1 且无 Observer GC Root → 泄漏已修复,进入 4.6 预防方案。
若 Dump Heap 中目标类实例数 > 1,且 Path to GC Root 仍包含 Observer → 修复未生效,返回 4.4 重新检查代码;
若实例数 ≤ 1 且无 Observer GC Root → 泄漏已修复,进入 4.6 预防方案。
4.6 预防方案
从编码规范、静态检查和团队流程三个层面防止同类问题再次发生。
- 编码规范:
- 禁止在 Activity/Fragment 中使用
observeForever()注册匿名内部类 Observer;确需使用时,必须声明为字段并在onDestroy()中移除。 - Fragment 中观察 LiveData 必须使用
getViewLifecycleOwner(),禁止使用getActivity()作为 LifecycleOwner。 - 匿名内部类优先改为静态内部类 + WeakReference 持有外部引用。
- 禁止在 Activity/Fragment 中使用
- 静态检查:
- 在 Android Lint 配置中启用
LiveDataObserverWithoutLifecycleOwner检查。 - 使用 LeakCanary 集成测试,在 CI 流程中自动检测泄漏。
- 在 Android Lint 配置中启用
- 代码审查清单:
- 所有
observe()调用是否传入正确的 LifecycleOwner? - 所有
observeForever()是否有对应的removeObserver()? - Fragment 中的 Observer 是否可能持有已销毁的 View 引用?
- 所有
推荐实践:
在团队项目中推广 “LiveData 观察三原则”——
① 能用 observe() 绝不用 observeForever();
② Fragment 永远使用 getViewLifecycleOwner();
③ 所有 Observer 优先写成静态内部类或 lambda(非匿名内部类)。
在团队项目中推广 “LiveData 观察三原则”——
① 能用 observe() 绝不用 observeForever();
② Fragment 永远使用 getViewLifecycleOwner();
③ 所有 Observer 优先写成静态内部类或 lambda(非匿名内部类)。