匿名内部类持有外部 Activity 引用是常见的内存泄漏原因,请举例说明并给出解决方案
解读
国内面试中,这道题几乎必问,因为它同时考察三点:
- 对“Java 语言层面引用关系”的理解(匿名内部类默认持有外部类实例的强引用);
- 对“Activity 生命周期”与“GC Root 可达性”的敏感度;
- 对“Android 特有线程组件(Handler、Timer、RxJava、协程、网络回调等)”的实战经验。
面试官希望你用“代码片段 + 引用链分析 + 修复方案”三步走,把“为什么泄漏、怎么泄漏、怎么不泄漏”讲透。答得太浅(只提 static)会被追问“还有吗”,答得太深(直接扯 MAT、Shark)又容易超时,因此要把“场景、根因、工具、官方 API”讲到恰到好处。
知识点
- 匿名内部类编译后生成
Outer$this字段,强引用外部实例。 - Activity 被销毁后,若该引用链仍被 GC Root 持有(运行中的线程、MessageQueue、等待执行的 Runnable、未解除的观察者等),则整个 Activity 布局树与 Bitmap 无法回收,造成数十 MB 级泄漏。
- 国内四大典型场景:
- Handler.postDelayed 匿名 Runnable
- Timer/TimerTask
- Retrofit 回调
- 自建线程池 / 协程作用域
- 检测工具:LeakCanary 2.x(自动安装、无性能损耗)、Profiler Heap Dump + “Nearest GC Root” 路径、Perfetto 跟踪 Binder 引用。
- 官方推荐修复套路:
- 切断引用:静态内部类 + WeakReference;
- 提前清理:onDestroy 时 removeCallbacksAndMessages、dispose、cancel;
- 生命周期感知:LifecycleOwner + LifecycleCoroutineScope、ViewModel、LiveData。
答案
【代码举例】
以下代码在 2022 年某大厂线上崩溃系统中出现频率 Top3:
public class MainActivity extends AppCompatActivity {
private final Handler mHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 匿名内部类 Runnable 持有外部 MainActivity 引用
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "延迟提示", Toast.LENGTH_SHORT).show();
}
}, 5000); // 用户 2 s 后按返回键退出,Activity 无法回收
}
}
【泄漏根因】
MessageQueue 中的 Message.target = Handler,Message.obj = Runnable,Runnable 的编译期合成字段 this$0 指向已销毁的 MainActivity,导致整条链被主线程 GC Root 持有。
【修复方案一:静态内部类 + WeakReference】
public class MainActivity extends AppCompatActivity {
private final Handler mHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(new MyRunnable(this), 5000);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null); // 保险清理
}
private static class MyRunnable implements Runnable {
private final WeakReference<MainActivity> actRef;
MyRunnable(MainActivity act) { actRef = new WeakReference<>(act); }
@Override
public void run() {
MainActivity act = actRef.get();
if (act != null && !act.isFinishing()) {
Toast.makeText(act, "延迟提示", Toast.LENGTH_SHORT).show();
}
}
}
}
【修复方案二:Lifecycle 感知协程(Kotlin)】
lifecycleScope.launch {
delay(5000)
if (isActive) { // 作用域已取消则自动返回
toast("延迟提示")
}
}
【修复方案三:RxJava + AutoDispose】
Observable.timer(5, TimeUnit.SECONDS)
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
.subscribe(t -> toast("延迟提示"));
【结论】
核心思路只有一句:把“匿名内部类默认强引用”变成“生命周期可感知、可取消、弱引用”三者之一,就能彻底消除泄漏。
拓展思考
- 为什么“静态内部类”能切断引用?—— 编译器不再生成
this$0,字节码层面与外部实例解耦。 - WeakReference 是否 100% 安全?—— 极端低内存场景下可能提前被回收,需在业务层做空保护;若任务必须执行,可用 Application Context + 前台 Service。
- LeakCanary 2.x 在国内 ROM 的适配坑:
- 部分厂商把 “content provider auto-registration” 裁剪,需手动
LeakCanary.config = Config(...); - 悬浮窗权限被禁,可降级为
LeakCanary.showNotifications = false,仅日志上传后台。
- 部分厂商把 “content provider auto-registration” 裁剪,需手动
- 除了匿名内部类,Kotlin 的
object : XXX {}同样持有外部引用,写法更隐蔽;Jetpack Compose 的remember { }若引用了 Activity 也会泄漏,需用rememberSaveable或ViewModel级作用域。 - 面试加分项:把“泄漏检测”做到 CI,利用 GitHub Action + Gradle Task 每周跑一次
./gradlew leakcanary-android:connectedCheck,自动生成 Leak 报告并上传到飞书群,体现工程质量意识。