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 背后,一不小心就漏了。
我个人习惯用一个“生命周期对齐”的思路:
- 在
onResume或Lifecycle.State.STARTED时绑定 UseCase - 在
onPause或onDestroy时解绑或关闭流 - 对
ImageProxy,一定在分析结束后立即 close - 对
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 资源从创建到泄漏的关键路径。你可以看到,每个箭头都代表一个需要被管理的引用或生命周期。
💡 个人技巧: 我习惯在 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)来定位问题。但基础打牢了,后面才不会被绕晕。