4. 泄漏根因分类:未解绑、静态内部类隐式持有、匿名内部类、单例持有Context
动态注册的 BroadcastReceiver 泄漏,本质上都是 Activity / Context 引用被外部对象长周期持有,导致生命周期结束时无法回收。根据引用持有方式的不同,可归纳为以下四类根因:
核心判断规则:只要
BroadcastReceiver 注册时的 Context 是一个短生命周期对象(如 Activity),并且该 Receiver 被一个生命周期更长的对象(如 Application、静态变量、单例)持有引用,则必然发生泄漏。反之,如果 Receiver 与 Context 生命周期一致且及时解绑,则安全。
4.1 未解绑(Missing unregisterReceiver)
| 阶段 | 操作要点 |
|---|---|
| 现象识别 | Activity 退出后,BroadcastReceiver 仍能收到广播;或反复进入退出界面后发生 OutOfMemoryError。 |
| 数据采集 | 通过 adb shell dumpsys meminfo [package] 观察 Activity 实例计数;使用 MAT / Profile 查看 BroadcastReceiver 对象引用路径。 |
| 原因定位 | 检查 onPause() / onDestroy() 中是否调用 unregisterReceiver();生命周期配对规则:一次 registerReceiver 必须对应一次 unregisterReceiver。 |
| 修复动作 | 在 onPause() 中执行 unregisterReceiver(mReceiver),建议使用 ContextWrapper 的 unregisterReceiver 方法。 |
| 复测 | 快速进出 Activity 5~10 次,使用 dumpsys meminfo 确认 Activity 实例数不增长,且 Receiver 对象无泄漏。 |
| 预防 | 使用基类 BaseActivity 统一处理注册/解绑;或采用生命周期感知组件(如 LifecycleObserver)自动解绑。 |
典型错误代码: 在
onResume() 注册,但 onPause() 中漏掉 unregisterReceiver;或异常路径未解绑(如 onSaveInstanceState 后未恢复解绑)。
// ❌ 错误示例:注册后没有对应的解绑
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
registerReceiver(mBatteryReceiver, filter);
// 漏掉了 onPause 中的 unregisterReceiver
}
// ✅ 正确示例:onPause 中解绑
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
registerReceiver(mBatteryReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mBatteryReceiver);
}
4.2 静态内部类隐式持有
当 BroadcastReceiver 被定义为 static inner class 时,它本身不持有外部 Activity 引用。但若该静态内部类中使用了外部类的成员变量(如 TextView、View),则会隐式地持有外部类的 this 引用,导致 Activity 无法 GC。
| 阶段 | 操作要点 |
|---|---|
| 现象识别 | Memory Analyzer 中看到 Activity 对象存在一条引用链:StaticInnerClassReceiver → Activity.this。 |
| 数据采集 | 使用 Eclipse MAT 的 “Path to GC Roots” 功能,排除弱引用后查看引用链;或 Android Studio Profiler 的堆转储分析。 |
| 原因定位 | 静态内部类中凡是访问外部类的 非静态 字段或方法,都会生成一个 this$0 字段引用外部实例。 |
| 修复动作 | 将静态内部类改为 非静态内部类 并使用弱引用 WeakReference<Activity>;或改为顶层类通过构造函数传入 Context。 |
| 复测 | 重复打开/关闭 Activity,用 adb shell dumpsys meminfo 观察 Activity 实例数保持为零或 1(合法状态)。 |
| 预防 | 坚持静态内部类不访问外部类实例成员;若必须访问,使用 WeakReference 或 Application Context。 |
// ❌ 错误:静态内部类访问了外部 Activity 的字段(形成隐式持有)
public class MainActivity extends Activity {
private TextView mStatusText;
static class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 实际持有 MainActivity.this
mStatusText.setText("Received");
}
}
}
// ✅ 修复:使用 WeakReference 解除强引用
public class MainActivity extends Activity {
private TextView mStatusText;
static class MyReceiver extends BroadcastReceiver {
private WeakReference textRef;
MyReceiver(TextView tv) { textRef = new WeakReference<>(tv); }
@Override
public void onReceive(Context context, Intent intent) {
TextView tv = textRef.get();
if (tv != null) tv.setText("Received");
}
}
}
4.3 匿名内部类
在 registerReceiver 中直接 new BroadcastReceiver() {...} 创建的匿名内部类会隐式地持有外部 Activity 的引用。如果该 Receiver 被注册为 静态变量 或 持久化的成员变量,则会导致泄漏。
| 阶段 | 操作要点 |
|---|---|
| 现象识别 | Profiler 中看到 anonymous class $0 持有 Activity;并且该 Receiver 在 Activity 销毁后仍被全局引用链可达。 |
| 数据采集 | 使用 adb shell dumpsys activity broadcasts 查看已注册的 Receiver 列表;MAT 查找 $AppBroadcastManager 中的引用。 |
| 原因定位 | 匿名内部类实例被赋值给成员变量或静态变量,且未在 onPause 中解绑,导致 Activity 引用链保持。 |
| 修复动作 | 将匿名内部类提取为 非静态内部类 或 顶层类,并确保生命周期管理正确;避免将 Receiver 实例存入静态集合。 |
| 复测 | 编写单元测试模拟 Activity 重建,检查堆转储中 Activity 对象数,确保无泄漏。 |
| 预防 | 禁止在 Application 或 Activity 中以匿名类形式注册动态 Receiver,除非明确它不会逃逸。 |
// ❌ 错误:匿名内部类 + 静态变量持有导致泄漏
public class MainActivity extends Activity {
private static BroadcastReceiver sReceiver; // 静态变量长生命周期
@Override
protected void onResume() {
super.onResume();
sReceiver = new BroadcastReceiver() { // 匿名内部类持有 MainActivity.this
@Override
public void onReceive(Context context, Intent intent) {
// do something
}
};
registerReceiver(sReceiver, new IntentFilter("com.example.CUSTOM"));
}
@Override
protected void onPause() {
super.onPause();
// 如果有解绑,但 sReceiver 仍被静态变量持有
unregisterReceiver(sReceiver);
}
// 即使解绑,静态变量仍引用了这个匿名 Receiver 对象,阻止其回收
}
// ✅ 修复:避免静态变量;或使用非匿名类并在 onDestroy 中置 null
public class MainActivity extends Activity {
private BroadcastReceiver mReceiver;
@Override
protected void onResume() {
super.onResume();
mReceiver = new MyReceiver();
registerReceiver(mReceiver, new IntentFilter("com.example.CUSTOM"));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
mReceiver = null; // 帮助 GC
}
private static class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) { }
}
}
4.4 单例持有 Context
单例模式中持有 Activity 类型的 Context,会使 Activity 及其关联的 BroadcastReceiver 永远无法被回收。这是最常见且影响最严重的泄漏类型。
| 阶段 | 操作要点 |
|---|---|
| 现象识别 | App 内存持续上涨,退出所有 Activity 后仍有大量 Activity 实例残留;dumpsys meminfo 中 Activity 数量异常。 |
| 数据采集 | 使用 MAT 的 “Biggest Objects” 查看单例对象;检查 Context 字段的引用路径。 |
| 原因定位 | 单例类的 static 字段保存了 Activity.this 或 getApplicationContext() 以外的非应用级 Context。 |
| 修复动作 | 单例中只持有 Application 类型的 Context(context.getApplicationContext()),或使用 WeakReference 包装。 |
| 复测 | 反复进出 Activity,触发 GC 后使用 adb shell dumpsys meminfo 确认 Activity 对象数归零。 |
| 预防 | 代码审查中禁止单例类直接存储 Activity 或 Service 类型的 Context;推荐使用 Application Context 简化生命周期。 |
// ❌ 错误:单例持有 Activity Context
public class NetworkManager {
private static NetworkManager sInstance;
private Context mContext;
private NetworkManager(Context context) {
mContext = context; // 如果传入的是 Activity,则 Activity 泄漏
}
public static NetworkManager getInstance(Context context) {
if (sInstance == null) sInstance = new NetworkManager(context);
return sInstance;
}
public void registerNetworkReceiver() {
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
mContext.registerReceiver(new NetworkReceiver(), filter);
}
}
// ✅ 修复:单例使用 Application Context
public class NetworkManager {
private static NetworkManager sInstance;
private Context mAppContext;
private NetworkManager(Context context) {
mAppContext = context.getApplicationContext(); // 安全
}
// ...
}
四类泄漏根因对比速查表
| 根因 | 持有方式 | 典型特征 | 最简修复 |
|---|---|---|---|
| 未解绑 | Activity 本身持有 Receiver | 缺少配对 unregisterReceiver | 在 onPause 中解绑 |
| 静态内部类隐式持有 | 静态类中的 this$0 指向外部实例 | 静态内部类访问了外部类成员 | 使用 WeakReference 或改为非静态 |
| 匿名内部类 | 匿名类实例 + 长生命周期持有者 | 注册到静态变量/全局集合中 | 提取为命名内部类并避免静态 |
| 单例持有 Context | 单例对象的字段 | 单例存储了 Activity 实例 | 替换为 Application Context |
排查捷径: 一旦在 MAT 中看到
Context 的引用链指向 AppBroadcastManager.mReceivers,优先怀疑 未解绑;如果引用链指向 static 字段,则检查 单例 或 静态内部类;如果匿名类出现在堆中且无解绑,则定位 匿名内部类。