CameraX 简介与资源管理基础

做 Android 相机开发这么多年,我见过太多内存泄漏的案例。有的来自原生 Camera2,有的来自第三方库。但 CameraX 的出现,本来是为了帮我们省事的——结果很多人反而因为它“太方便”而忽略了资源管理,踩了不少坑。

说实话,CameraX 的设计确实优雅。它把 Camera2 那些繁琐的回调、状态机、session 管理都封装好了,你只需要关心业务逻辑。但越是这种“黑盒”方案,越容易在资源释放上出问题。我就在项目里遇到过:一个拍照页面退出后,Camera 的预览流还在跑,内存一直涨,直到 OOM。

所以这一章,我们先从 CameraX 的资源管理基础聊起。搞清楚它到底管理了哪些资源,以及为什么容易泄漏。

什么是 CameraX?

CameraX 是 Google 官方推出的 Jetpack 相机库。它的核心目标是简化相机开发,同时保持向后兼容。说白了,就是用一套统一 API 搞定不同 Android 版本的相机实现。

它提供了三个核心用例:

  • Preview:预览取景器画面
  • ImageCapture:拍照
  • ImageAnalysis:实时分析帧数据(比如扫码、人脸检测)

你还可以把它们组合起来用。例如同时绑定 Preview 和 ImageAnalysis,边预览边分析。CameraX 内部会帮你协调数据流,但**资源管理这块,还是得我们自己操心**。

CameraX 的“资源”到底是什么?

很多人以为资源就是“Camera 硬件”。其实远不止。我列一下常见的:

组件 资源类型 泄漏后果
ProcessCameraProvider Camera HAL 连接、Camera ID 绑定 无法再次打开相机,强行打开可能崩溃
LifecycleOwner Activity/Fragment 生命周期观察者 组件不销毁,页面泄漏
ImageProxy 图像数据缓冲区(字节数组、YUV 等) 内存持续增长,OOM
UseCaseGroup 内部线程池、Surface 资源 线程泄漏,CPU 占用升高
CameraController(ViewBinding 方案) 内部 PreviewView 的 Surface 界面不释放,view 泄漏

嗯,这里面最常见也最隐蔽的,就是 ImageProxy 不关闭。我早期有个项目,在 ImageAnalysis.Analyzer 里直接丢了个匿名内部类,忘了调 image.close(),结果每帧都泄漏一块 YUV 数据。那个量级小,但积累起来很恐怖。

资源管理的核心原则

其实就一句话:谁创建,谁释放。但 CameraX 的创建过程往往隐藏在 bindToLifecycle 和 ProcessCameraProvider 背后,一不小心就漏了。

我个人习惯用一个“生命周期对齐”的思路:

  1. 在 onResume 或 Lifecycle.State.STARTED 时绑定 UseCase
  2. 在 onPause 或 onDestroy 时解绑或关闭流
  3. 对 ImageProxy,一定在分析结束后立即 close
  4. 对 ProcessCameraProvider,不要重复获取实例

你看,是不是很简单?但实际项目里,因为 Fragment 的生命周期被嵌套、被缓存、被 ViewPager 复用,这些规则很容易被打破。避坑指南来了:我曾经在 ViewPager2 + FragmentStateAdapter 的场景里,因为 Fragment 被复用但 CameraX 没有重新解绑,导致页面切回来时预览黑屏,同时内存泄漏。 后来我强制在 onDestroyView 里调用 cameraProvider.unbindAll() 才解决。

一个简单的 CameraX 用例代码

// 典型绑定代码
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

cameraProviderFuture.addListener({
    val cameraProvider = cameraProviderFuture.get()
    val preview = Preview.Builder().build()
    val imageCapture = ImageCapture.Builder().build()

    // 注意:这里绑定了 LifecycleOwner
    val camera = cameraProvider.bindToLifecycle(
        lifecycleOwner,
        CameraSelector.DEFAULT_BACK_CAMERA,
        preview,
        imageCapture
    )
}, ContextCompat.getMainExecutor(context))

这段代码看起来干净。但想一想:如果 lifecycleOwner 已经 destroyed,而 cameraProvider 还被某个异步任务持有,会发生什么?没错,Camera 资源没释放。

⚠️ 警告: bindToLifecycle 内部会监听 LifecycleOwner 的销毁事件来自动解绑。但如果你的 LifecycleOwner 是 Activity 但 Activity 被干掉的同时还保留着 cameraProvider 的引用,那么自动解绑不会执行——因为 Activity 的 Lifecycle 已经结束了,但 CameraProvider 的引用还在另一个线程里。这种情况多发生在使用 ListenableFuture 时没有清理回调。

所以,**不要完全依赖自动解绑**。我建议在 onDestroy 中显式调用 cameraProviderFuture.cancel(true),或者至少检查 isDestroyed 状态。

资源管理的层次结构

下面这张图是我自己总结的,画出了 CameraX 资源从创建到泄漏的关键路径。你可以看到,每个箭头都代表一个需要被管理的引用或生命周期。

CameraX 架构与资源泄漏风险点 ProcessCameraProvider 获取一次,全局共享 LifecycleOwner Activity / Fragment bindToLifecycle() 自动解绑?未必可靠 生命周期绑定 UseCase 组 Preview · ImageCapture · ImageAnalysis ImageProxy 每帧数据缓冲区 创建/绑定 🔴 常见泄漏点:ImageProxy 未关闭、LifecycleOwner 被强引用、unbind 遗漏 建议:在 onDestroy 显式调用 unbindAll() 并关闭所有 ImageProxy 生命周期结束 资源管理口诀 谁拿谁还,生命周期对齐,异步回调要清理,ImageProxy 用完即关 —— 来自一个踩过多次坑的过来人

💡 个人技巧: 我习惯在 Application 里用一个全局的 CameraXConfig 配置,然后在每个页面的 onCreate 里单独获取 ProcessCameraProvider 实例,并在 onDestroy 里调用 ProcessCameraProvider::unbindAll。这样即使 Lifecycle 自动解绑失败,手动解绑也能兜底。

为什么资源泄漏在 CameraX 中尤其致命?

Camera 硬件资源是独占的。一个进程只能打开一台摄像头(基本如此)。如果你的 Activity 没释放 Camera,那么再次打开同一个摄像头就会抛出 CameraAccessException,甚至直接闪退。而且 Camera 的底层 buffer 是共享内存,泄漏后 GC 无法回收,只能靠杀掉进程。

另外,CameraX 内部使用了 HandlerThread 和 ExecutorService。如果这些线程没有被正确关闭,它们会一直存活,导致应用的后台线程数不断增长。我见过一个线上 bug:用户频繁切换相机页面,线程数飙到 200 多,手机直接卡死。

所以,**资源管理不只是面子工程,它是 App 稳定性的底线。**

本章小结

CameraX 虽然简化了相机开发,但资源管理仍然需要我们自己上心。核心就是抓住三个关键:

  • 生命周期遵守:绑定/解绑要与 Activity/Fragment 对齐
  • ImageProxy 一定要 close
  • 全局的 CameraProvider 不要滥用,用完就清理掉引用

下一部分我们会深入具体的泄漏场景,用真正的内存分析工具(MAT、LeakCanary)来定位问题。但基础打牢了,后面才不会被绕晕。