2. 注册链路快照:获取 registerReceiver 调用栈、上下文引用链、注册时机

排查阶段定位:本章节覆盖从“现象识别”到“预防措施”的完整闭环,核心输出是一份可执行的“注册链路快照”采集与分析方法,帮助开发者在几分钟内锁定泄漏源头。

2.1 现象识别:何时需要采集注册链路快照

并非所有寄存器调用都意味着泄漏。以下“危险信号”触发快照采集流程:

现象特征 判断规则 优先级
Activity 销毁后 Receiver 仍响应广播 在 onDestroy 中未配对 unregisterReceiver P0 致命
MAT / LeakCanary 报告 Receiver 对象泄漏 GC root 链指向 Activity 或已销毁 Fragment P0 致命
Receiver 数量随页面切换持续增长 adb shell dumpsys activity 显示 Receiver 列表膨胀 P1 严重
Context 被 Receiver 隐性持有导致内存泄漏 Context 引用链超过 3 跳且包含 android.app.Activity P1 严重
注册时机在 onCreate / onResume 高频重复注册 同一 Receiver 对象被重复注册到不同 IntentFilter P2 一般
采集触发条件:上表中任意一条命中,立即执行本章的“三要素快照”流程。不要等到 OOM 或 ANR 出现才行动。

2.2 数据采集:三要素捕获技术

三要素即“调用栈 + 上下文引用链 + 注册时机”。三者互为印证,缺少任何一项都会导致定位偏差。

2.2.1 调用栈捕获——谁调用了 registerReceiver

使用动态代理或 Debug API 在 registerReceiver 入口处注入钩子,记录完整调用栈。

// 方案一:通过 Debug.getStackTrace() 在代理 Receiver 中捕获
public class CaptureReceiver extends BroadcastReceiver {
    private final BroadcastReceiver original;
    private final StackTraceElement[] captureStack;

    public CaptureReceiver(BroadcastReceiver original) {
        this.original = original;
        // 在构造时抓取注册调用栈
        this.captureStack = Thread.currentThread().getStackTrace();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // 如果 original 为 null,说明已被回收,此处为泄漏点
        if (original == null) {
            logLeakReport(captureStack, context);
            return;
        }
        original.onReceive(context, intent);
    }

    private void logLeakReport(StackTraceElement[] stack, Context ctx) {
        StringBuilder sb = new StringBuilder("[ReceiverLeak] 注册调用栈:\n");
        for (StackTraceElement e : stack) {
            sb.append("    at ").append(e.toString()).append("\n");
        }
        sb.append("Context 类型: ").append(ctx.getClass().getName());
        Log.e("LeakTrace", sb.toString());
    }
}
注意:不要在 onReceive 中做耗时操作;捕获栈信息后异步写入文件或内存缓冲区,避免阻塞广播分发。

2.2.2 上下文引用链提取——Context 是否持有 Activity

通过反射或 LeakCanary 的 HeapAnalyzer 提取 Context 的引用路径。关键判断规则:

Context 来源 引用链特征 泄漏风险
Activity.this Activity → mBase → ContextImpl → ReceiverDispatcher 高
getApplicationContext() Application → ContextImpl → ReceiverDispatcher 低(单例生命周期)
Service.this Service → mBase → ContextImpl → ReceiverDispatcher 中(取决于 Service 生命周期)
View.getContext() View → Activity → ContextImpl → ReceiverDispatcher 高(隐式持有 Activity)
// 快速检测 Context 引用的辅助函数
public static boolean isHoldingActivity(Context context) {
    if (context == null) return false;
    Class clazz = context.getClass();
    // 逐层检查包装链:ContextWrapper → Activity / Service / Application
    while (clazz != null) {
        if (Activity.class.isAssignableFrom(clazz)) {
            return true;
        }
        clazz = clazz.getSuperclass();
    }
    return false;
}

// 在注册点插入检测
if (isHoldingActivity(context)) {
    Log.w("ReceiverLeak", "使用 Activity Context 注册 Receiver,请确认已配对注销");
}

2.2.3 注册时机判定——发生在哪个生命周期阶段

通过 AOP 切面或手动埋点标记注册时的生命周期阶段。推荐在基类 BaseActivity 中统一插桩:

// 基类中记录当前生命周期状态
public enum LifecyclePhase {
    NONE, ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY
}

public class BaseActivity extends AppCompatActivity {
    protected LifecyclePhase currentPhase = LifecyclePhase.NONE;

    @Override
    protected void onCreate(Bundle sis) {
        super.onCreate(sis);
        currentPhase = LifecyclePhase.ON_CREATE;
    }

    @Override
    protected void onResume() {
        super.onResume();
        currentPhase = LifecyclePhase.ON_RESUME;
    }

    @Override
    protected void onDestroy() {
        currentPhase = LifecyclePhase.ON_DESTROY;
        super.onDestroy();
    }

    // 统一的注册方法,自动记录时机
    public void safeRegisterReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        Log.d("RegisterTrace", "注册 Receiver [" + receiver.getClass().getSimpleName()
            + "] 于阶段: " + currentPhase
            + " 调用栈: " + Debug.getStackTrace());
        registerReceiver(receiver, filter);
    }
}
判断规则:ON_CREATE / ON_RESUME 注册必须在对应的 ON_DESTROY / ON_PAUSE 中注销。如果跨阶段未配对,直接标记为泄漏候选。

2.3 原因定位:从快照数据定位泄漏根因

将三要素快照数据合并为一张“泄漏决策树”进行判定:

注册链路快照采集与分析流程 注册链路快照 — 三要素采集判定流程 ① 发现泄漏信号 ② 捕获 registerReceiver 调用栈 ③ 提取 Context 引用链 ④ 判定注册生命周期阶段 ⑤ 三要素交叉判定泄漏风险 ⑥ 输出注册链路快照报告 ⑦ 生成精准修复建议 判定规则:调用栈显示 Activity 内部注册 + Context 引用链指向 Activity + 注册时机在 onResume 且未在 onPause 注销 → 100% 泄漏

综合判定表示例:

调用栈指向 Context 引用链 注册时机 判定结论
Activity.onResume Activity → ContextImpl ON_RESUME 高概率泄漏(未配对注销)
Fragment.onCreateView Fragment → Activity → ContextImpl ON_CREATE 中概率泄漏(Fragment 销毁后未清理)
Application.onCreate Application → ContextImpl NONE(应用级) 低概率泄漏(应用级生命周期)
Service.onStartCommand Service → ContextImpl ON_START 中概率泄漏(Service 停止后未注销)

2.4 修复动作:基于快照的精准修复

  1. 配对注销:在对应的生命周期反向点调用 unregisterReceiver。例如 onResume 注册 → onPause 注销;onCreate 注册 → onDestroy 注销。
  2. 替换 Context:将 Activity.this 替换为 getApplicationContext(),前提是 Receiver 不需要与 Activity 生命周期绑定。
  3. 弱引用包装:如果必须持有 Activity 引用,使用 WeakReference<Activity> 并在 onReceive 中判空。
  4. 取消重复注册:注册前检查 Receiver 是否已注册,避免同一 Receiver 被多次注册到相同 IntentFilter。
// 修复示例:使用 Application Context + 生命周期配对注销
public class SafeActivity extends BaseActivity {
    private BroadcastReceiver mReceiver;
    private IntentFilter mFilter;

    @Override
    protected void onResume() {
        super.onResume();
        mReceiver = new SafeReceiver();
        mFilter = new IntentFilter("com.example.MY_ACTION");
        // 使用 Application Context 注册
        getApplicationContext().registerReceiver(mReceiver, mFilter);
        Log.d("SafeRegister", "Receiver 已用 Application Context 注册");
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mReceiver != null) {
            getApplicationContext().unregisterReceiver(mReceiver);
            Log.d("SafeRegister", "Receiver 已注销");
            mReceiver = null;
            mFilter = null;
        }
    }
}
常见修复陷阱:
· 在 onStop 中注销但 onPause 中注册 → 配对不匹配,仍可能泄漏。
· 使用匿名内部类作为 Receiver 注册 → 无法在外部取消注册 → 必须改用成员变量。
· 在子线程中注册 Receiver → Context 可能被子线程持有 → 使用主线程 Handler 注册。

2.5 复测验证:修复效果确认

修复后执行以下三项验证,全部通过方可关闭工单:

验证项 方法 通过标准
内存快照对比 MAT / LeakCanary 对比修复前后的 Receiver 实例数 Receiver 实例数在 Activity 销毁后归零
广播响应正确性 发送测试广播,确认 onReceive 正常触发 功能无退化,广播不丢失
压力测试 快速切换 Activity 50 次,观察 Receiver 注册数 注册数不持续增长,稳定在基线值
# 快速检查 Receiver 注册数的 adb 命令
adb shell dumpsys activity receivers | grep -E "ReceiverList|RegisteredReceivers" | wc -l
# 修复前预期:每次页面切换 +1 ~ +3
# 修复后预期:页面切换后数量回落到初始值

2.6 预防措施:注册行为规范化

  • 统一注册入口:在基类 BaseActivity / BaseFragment 中提供 registerLifecycleReceiver(),自动记录生命周期阶段和调用栈。
  • 代码审查规则:禁止在匿名内部类、Lambda 中直接调用 registerReceiver;必须通过成员变量 + 显式注销。
  • 自动化检测工具:集成自定义 Lint 规则或 ByteX 插件,在编译期扫描未配对的 register/unregister 调用。
  • 监控告警:线上接入 Receiver 注册数监控,当单进程注册数超过阈值(如 50)时触发告警并采集快照。