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 对比了泄漏场景与修复场景的引用路径:

BroadcastReceiver 动态注册 — 强引用链泄漏 vs 修复 ❌ 泄漏场景 GC Roots (系统服务) 强引用 AMS 持有 Receiver 注册表 强引用 BroadcastReceiver 对象 隐式持有外部 Activity Activity 实例 ➜ 无法 GC ❌ ✅ 修复场景 onDestroy() 入口 unregisterReceiver() 从系统服务移除 Receiver 引用链断开 ✓ Activity 可回收 ✓ 解注册的本质 从系统服务(AMS)的注册表中移除 Receiver 对象,切断 GC Roots → Activity 的整条强引用路径

从 SVG 可以看出,泄漏的充分必要条件:

  1. GC Roots 可达:系统服务(AMS)通过注册表持有 Receiver 对象;
  2. 隐式外部引用:Receiver 是匿名 / 非静态内部类,自动持有外部 Activity 的 this 引用;
  3. 未执行断链操作: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 门禁。