3. 引用链分析:理解注册后 Receiver 与 Context、Activity/Service 的强引用关系
动态注册 BroadcastReceiver 导致的内存泄漏,本质是一条从 GC Roots 到 Activity/Service 实例的 强引用链 未被断开。本章从排查视角拆解这条链的构成,帮助你在堆转储中快速定位泄漏源头。
3.1 现象识别:内存泄漏的早期信号
当 Activity/Service 因 Receiver 注册发生泄漏时,通常表现出以下可观测特征:
| 序号 | 现象 | 可量化指标 | 误判排除 |
|---|---|---|---|
| ① | 反复进入退出 Activity 后,内存占用量阶梯式上升 | 每次进入退出,内存增量 ≥ 该 Activity 正常占用 | 排除图片缓存、单例数据池 |
| ② | 手动触发 GC(Profile 中点击 "Force GC")后,内存不回落 | GC 后 retained size 仍 ≥ Activity 对象大小 | 排除静态集合、长时间后台任务 |
| ③ | LeakCanary 通知中显示 "BroadcastReceiver" 或 "Context" 类位于引用链 | 泄漏分析报告中出现 registerReceiver 调用栈 |
排除其他注册式泄漏(如 ContentObserver) |
排查判断规则: 同时满足 ① + ②,且 ③ 中引用链包含
android.content.BroadcastReceiver 子类时,即可初步锁定为 Receiver 动态注册泄漏。
3.2 数据采集:捕获引用链证据
使用以下工具链采集泄漏证据,每一层输出都是下一层的输入:
| 步骤 | 工具 / 命令 | 采集目标 | 关键输出 |
|---|---|---|---|
| 1 | Android Profiler → Memory → Dump Java Heap | 当前堆中所有 Activity 实例 | Activity 对象数量 > 1(期望值为 1) |
| 2 | MAT / LeakCanary 分析 hprof | Activity 到 GC Roots 的最短路径 | 引用链路径,确认是否存在 BroadcastReceiver 节点 |
| 3 | adb shell dumpsys activity broadcasts |
当前已注册的全部动态 Receiver | 接收器列表,确认泄漏的 Receiver 仍存活 |
| 4 | adb shell dumpsys meminfo <package> |
进程内存概览 | Views / Activities 数量异常 |
步骤 3 的具体命令和典型输出示例:
# 查看当前进程所有已注册的动态广播接收器
adb shell dumpsys activity broadcasts | grep -A 5 "Registered Receivers"
# 典型输出(泄漏场景):
Registered Receivers:
ReceiverList{... type:dynamic pid:12345 uid:10001}
Filter{...}
Receiver: com.example.MyActivity$1@0x12a34b56
Context: com.example.MyActivity@0x56b78c90
采集避坑: 执行 dumpsys 前,确保目标 Activity 已处于 destroyed 状态(已调用 onDestroy 但进程未终止)。若在 Profiler 中看到 Activity 实例数 > 1,先手动触发一次 GC 再 dump,过滤掉软/弱引用可回收对象。
3.3 原因定位:引用链深度解析
动态注册泄漏的引用链由 4 个关键节点 串联而成。以下 SVG 对比了泄漏场景与修复场景的引用路径:
从 SVG 可以看出,泄漏的充分必要条件:
- GC Roots 可达:系统服务(AMS)通过注册表持有 Receiver 对象;
- 隐式外部引用:Receiver 是匿名 / 非静态内部类,自动持有外部 Activity 的
this引用; - 未执行断链操作:
onDestroy()中未调用unregisterReceiver()。
判断规则: 在 LeakCanary / MAT 的引用链中,如果出现
android.app.Activity → android.content.BroadcastReceiver → com.android.server.am.ActivityManagerService 的路径,且 Receiver 是匿名内部类,则 100% 确认 为动态注册泄漏。
3.4 修复动作:断链操作
修复的核心是 确保 Receiver 的生命周期与宿主对称。以下对比展示泄漏代码与修复代码:
| 方案 | 注册方式 | 注销时机 | 引用链状态 | 推荐场景 |
|---|---|---|---|---|
| ❌ 泄漏方案 | 匿名内部类,onStart() 中注册 |
未注销 | AMS → Receiver → Activity (强引用) | — |
| ✅ 标准修复 | 成员变量持有 Receiver 实例 | onStop() 或 onDestroy() 中注销 |
引用链完整断开 | 大多数 Activity / Service |
| ✅ 进阶修复 | 嵌套类 + WeakReference<Context> | onStop() 中注销 |
Receiver 不直接持有 Activity | 需要延时操作的场景 |
| ✅ 自动化修复 | LifecycleObserver 自动管理 | Lifecycle.Event.ON_STOP 触发注销 | 由框架保证对称性 | AndroidX 项目 |
标准修复代码示例:
public class MainActivity extends AppCompatActivity {
private BroadcastReceiver mReceiver; // 成员变量,避免匿名内部类隐式持有
@Override
protected void onStart() {
super.onStart();
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 处理广播
}
};
registerReceiver(mReceiver, new IntentFilter("com.example.MY_ACTION"));
}
@Override
protected void onStop() {
super.onStop();
if (mReceiver != null) {
unregisterReceiver(mReceiver); // 断链操作
mReceiver = null; // 辅助 GC
}
}
}
最佳实践: 使用
LifecycleObserver 自动化管理,彻底消除人为遗漏。通过 lifecycle.addObserver() 将注册/注销绑定到 Lifecycle 事件,即使开发人员忘记写 unregister,框架也会在 onStop 时自动执行。
3.5 复测:验证修复效果
修复后,按以下步骤执行验证:
| 步骤 | 操作 | 通过标准 |
|---|---|---|
| ① | 打开目标 Activity,按返回键退出 | Logcat 中看到 onStop() 和 onDestroy() 调用 |
| ② | 打开 Android Profiler,手动触发 Force GC | 内存曲线明显下降,回落至基线 |
| ③ | Dump Java Heap,搜索 Activity 类名 | Activity 实例数量 = 1(当前显示的实例) |
| ④ | adb shell dumpsys activity broadcasts |
已注册 Receiver 列表中不包含该 Activity 的 Receiver |
| ⑤ | 重复进入退出 10 次,观测内存 | 内存无阶梯式增长,每次 GC 后回到基线 |
复测陷阱: 如果步骤 ③ 中 Activity 实例数仍 > 1,但引用链中不包含 BroadcastReceiver,请排查其他泄漏源(如静态变量、Handler、View 附着的 WindowManager)。本章仅聚焦 Receiver 链,实际项目中可能存在复合泄漏。
3.6 预防:长效机制
将以下检查项纳入团队的代码审查和静态分析流程:
| 检查项 | 强制程度 | 自动化工具 | 违规示例 |
|---|---|---|---|
每个 registerReceiver() 必须有对应的 unregisterReceiver() |
P0 | Lint 规则 + Detekt | 只注册不注销 |
| Receiver 使用成员变量而非匿名内部类 | P0 | IDE Inspection "Anonymous class implicitly holds outer class" | registerReceiver(new BroadcastReceiver(){...}, filter) |
| 注册/注销位于对称的生命周期方法中(onStart/onStop 或 onResume/onPause) | P1 | 自定义 Lint 规则 | 在 onStart 注册但在 onDestroy 注销(对象已不可用) |
| 使用 LifecycleObserver 管理 Receiver | 推荐 | AndroidX 官方支持 | 手动管理但遗漏了分支路径 |
核心原则: 动态注册的 BroadcastReceiver,其 注册与注销必须成对出现,且注销时机不得晚于宿主的 onDestroy。将这条规则作为 Android 编码规范的 红线条款,纳入 CI 门禁。