匿名内部类持有外部 Activity 引用是常见的内存泄漏原因,请举例说明并给出解决方案

解读

国内面试中,这道题几乎必问,因为它同时考察三点:

  1. 对“Java 语言层面引用关系”的理解(匿名内部类默认持有外部类实例的强引用);
  2. 对“Activity 生命周期”与“GC Root 可达性”的敏感度;
  3. 对“Android 特有线程组件(Handler、Timer、RxJava、协程、网络回调等)”的实战经验。
    面试官希望你用“代码片段 + 引用链分析 + 修复方案”三步走,把“为什么泄漏、怎么泄漏、怎么不泄漏”讲透。答得太浅(只提 static)会被追问“还有吗”,答得太深(直接扯 MAT、Shark)又容易超时,因此要把“场景、根因、工具、官方 API”讲到恰到好处。

知识点

  1. 匿名内部类编译后生成 Outer$this 字段,强引用外部实例。
  2. Activity 被销毁后,若该引用链仍被 GC Root 持有(运行中的线程、MessageQueue、等待执行的 Runnable、未解除的观察者等),则整个 Activity 布局树与 Bitmap 无法回收,造成数十 MB 级泄漏。
  3. 国内四大典型场景:
    • Handler.postDelayed 匿名 Runnable
    • Timer/TimerTask
    • Retrofit 回调
    • 自建线程池 / 协程作用域
  4. 检测工具:LeakCanary 2.x(自动安装、无性能损耗)、Profiler Heap Dump + “Nearest GC Root” 路径、Perfetto 跟踪 Binder 引用。
  5. 官方推荐修复套路:
    • 切断引用:静态内部类 + 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("延迟提示"));

【结论】
核心思路只有一句:把“匿名内部类默认强引用”变成“生命周期可感知、可取消、弱引用”三者之一,就能彻底消除泄漏。

拓展思考

  1. 为什么“静态内部类”能切断引用?—— 编译器不再生成 this$0,字节码层面与外部实例解耦。
  2. WeakReference 是否 100% 安全?—— 极端低内存场景下可能提前被回收,需在业务层做空保护;若任务必须执行,可用 Application Context + 前台 Service。
  3. LeakCanary 2.x 在国内 ROM 的适配坑:
    • 部分厂商把 “content provider auto-registration” 裁剪,需手动 LeakCanary.config = Config(...)
    • 悬浮窗权限被禁,可降级为 LeakCanary.showNotifications = false,仅日志上传后台。
  4. 除了匿名内部类,Kotlin 的 object : XXX {} 同样持有外部引用,写法更隐蔽;Jetpack Compose 的 remember { } 若引用了 Activity 也会泄漏,需用 rememberSaveableViewModel 级作用域。
  5. 面试加分项:把“泄漏检测”做到 CI,利用 GitHub Action + Gradle Task 每周跑一次 ./gradlew leakcanary-android:connectedCheck,自动生成 Leak 报告并上传到飞书群,体现工程质量意识。