常见CameraX资源泄漏模式概述

CameraX的资源泄漏,说白了就是该释放的没释放。我排查过的线上问题里,90% 都逃不出下面这六种模式。今天咱们把它们掰开揉碎了讲清楚,后面实战的时候你心里就有谱了。

核心观点: 资源泄漏不是偶发 Bug,而是设计时埋下的坑。只要模式认清了,修复就是几分钟的事。
CameraX 资源泄漏六大模式总览 💡 六大模式覆盖 90% 以上的 CameraX 线上泄漏问题 CameraX 资源泄漏 生命周期泄漏 Image 泄漏 Provider 泄漏 Listener 泄漏 Session 泄漏 Executor 泄漏

1. 生命周期泄漏 —— 最常见的坑

Activity 或 Fragment 销毁了,CameraX 还在偷偷干活。这种情况最普遍,也最好修。

// 错误示例:onDestroy 里啥也没干
@Override
protected void onDestroy() {
    // ❌ 忘了调用以下任何一行
    // cameraProvider.unbindAll();
    // cameraProvider.shutdown();
    // lifecycle.removeObserver(myLifecycleObserver);
}

我记得有个社交App的扫码页面,每次打开内存涨 10MB+,关闭后一点不降。查了一圈,就是 onDestroy 里漏了 unbindAll。

避坑指南: 我曾经在一个 Fragment 嵌套的场景里,因为 parent Fragment 先销毁了,child 的 CameraX 绑定反而被提前切断,导致资源没释放干净。所以 一定要在 onDestroy 中显式调用 unbindAll + shutdown,别依赖 lifecycle 自动解绑。

2. Image 泄漏 —— 吃内存最快

ImageAnalysis 的 Analyzer 里,每一帧拿到的 ImageProxy 都必须 close。否则的话…… 1080p 分辨率下一帧就几 MB,30fps 跑个几秒,你想想看,几百 MB 就没了。

// 错误示例:ImageProxy 没 close
imageAnalysis.setAnalyzer(executor, imageProxy -> {
    byte[] buffer = ImageUtil.toByteArray(imageProxy);
    // 处理 buffer ...
    // ❌ 忘了调用 imageProxy.close()
});

// ✅ 正确做法:finally 里确保关闭
imageAnalysis.setAnalyzer(executor, imageProxy -> {
    try {
        byte[] buffer = ImageUtil.toByteArray(imageProxy);
        // 处理 buffer ...
    } finally {
        imageProxy.close();  // 必须 close!
    }
});

我在项目中遇到过电商 AR 试穿功能,上线后 OOM 频报。一查,Analyzer 里处理完图像直接 return 了,ImageProxy 根本没 close。修复后内存直接降了 70%。

小技巧: 建议用 try-finally 包裹整个 Analyzer 逻辑,养成肌肉记忆。另外如果你的 Analyzer 是异步的,记得把 close 放到异步回调里,别提前关。

3. Provider 泄漏 —— 单例不背锅

ProcessCameraProvider.getInstance() 返回的是进程级单例,这本身没问题。但很多人反复绑定却不解绑,导致内部监听器、Session 对象越积越多。

// 错误示例:重复绑定不考虑解绑
ProcessCameraProvider.getInstance(context, executor, provider -> {
    provider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis);
    // 下次调用又 bind 一次,旧的监听器还在
});

我个人习惯是在每次 bind 之前先调一次 unbindAll(),确保干净的绑定状态。

最佳实践: bind 前先 unbindAll,shutdown 后记得把 provider 引用置 null,避免长生命周期对象持有 Activity 引用。

4. Listener 泄漏 —— 看不见的引用链

CameraX 提供了很多回调:CameraStateCallback、CameraProviderCallback、VideoCapture.OnVideoSavedCallback…… 这些如果注册了没取消,会死死拽住你的 Activity 或 Fragment。

// 错误示例:注册了但没取消
camera.registerCameraStateCallback(executor, stateCallback);
// ❌ 在适当位置忘了取消
// camera.unregisterCameraStateCallback(stateCallback);

我曾经在一个直播项目里,切换摄像头越来越卡,dumpsys meminfo 一看,CameraStateCallback 注册了 20 多个。就是因为每次切换摄像头 new 了新的 callback,旧的没解绑。

避坑指南: 所有的注册操作,一定要配对出现。我建议用 LifecycleObserver 来统一管理注册和注销,省心很多。

5. Session 泄漏 —— 用例残留

当你重复调用 bindToLifecycle 或者 start/stop 相机时,旧的 UseCase Session 如果没有被正确关闭,就会一直留在内存里。这种泄漏比较隐蔽,通常伴随着 Camera 设备的异常占用。

// 错误示例:反复 start/stop 不清理
camera.start();  // 内部创建新 Session
camera.stop();   // 如果不彻底销毁 Session,旧 Session 会残留
camera.start();  // 又创建一个新 Session

这种情况在快速切换前后摄像头的场景下特别容易触发。我个人建议在 start 之前强制 unbindAll,确保没有旧 Session 残留。

6. Executor 泄漏 —— 线程在后台"长生不老"

CameraX 允许传入自定义 Executor。很多人传了一个 ExecutorService 进去,用完却忘了 shutdown。结果就是线程池一直在后台挂着,GC 也回收不了。

// 错误示例:自定义 Executor 没关闭
ExecutorService cameraExecutor = Executors.newSingleThreadExecutor();
cameraProvider.bindToLifecycle(lifecycleOwner, selector, useCase, cameraExecutor);
// ❌ 在 onDestroy 中忘了调用 cameraExecutor.shutdown();

我个人觉得这种泄漏虽然不会立刻导致 OOM,但线程泄漏积累起来,App 会越来越卡顿。特别是低端机上,多几个僵尸线程,CPU 开销就上去了。

建议: 用 Activity 自带的 MainExecutor 或者创建一个跟随生命周期的 Executor,通过 LifecycleObserver 自动 shutdown。

六大模式速查表

泄漏模式 触发场景 风险等级 典型表现
生命周期泄漏 Activity/Fragment 销毁时未关闭相机 高 退出页面后相机持续占用,内存不降
Image 泄漏 ImageProxy 未 close 高 内存持续增长,几秒内涨数百 MB
Provider 泄漏 重复 bind 未解绑 中 监听器堆积,Provider 引用链变长
Listener 泄漏 回调注册后未取消 中 Activity 无法被 GC,dumpsys 看到残留 callback
Session 泄漏 反复 start/stop 或切换摄像头 中 相机设备被异常占用,切换卡顿
Executor 泄漏 自定义线程池未 shutdown 低 后台线程残留,长期积累导致卡顿

这六种模式,我把它们叫做 CameraX 泄漏的"六边形战士"。大部分线上问题,你照着这个表去排查,基本都能找到根因。下一回遇到内存暴涨或者相机卡死,别慌,先看看是哪个模式在搞鬼。

总结一句话: 生命周期管好"生老病死",Image 盯紧 close,Provider 记得解绑,Listener 配对注册,Session 避免重复,Executor 及时 shutdown。做到这六点,CameraX 泄漏基本跟你无缘。