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 原因定位:从快照数据定位泄漏根因
将三要素快照数据合并为一张“泄漏决策树”进行判定:
综合判定表示例:
| 调用栈指向 | 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 修复动作:基于快照的精准修复
- 配对注销:在对应的生命周期反向点调用
unregisterReceiver。例如 onResume 注册 → onPause 注销;onCreate 注册 → onDestroy 注销。 - 替换 Context:将
Activity.this替换为getApplicationContext(),前提是 Receiver 不需要与 Activity 生命周期绑定。 - 弱引用包装:如果必须持有 Activity 引用,使用
WeakReference<Activity>并在 onReceive 中判空。 - 取消重复注册:注册前检查 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 注册。
· 在 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)时触发告警并采集快照。