匿名内部类持有外部 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 报告并上传到飞书群,体现工程质量意识。