第三讲:CameraX核心组件与生命周期绑定

好,我们直接进入正题。上一讲我们聊了CameraX的架构背景,今天我要重点讲清楚它的核心组件是如何跟Android生命周期绑定的。为什么要专门开一节来讲这个?

说白了,你如果不理解这个绑定机制,你的CameraX应用就会莫名其妙地泄漏——Activity销毁了相机还开着,ImageProxy占着内存不释放,预览Surface被僵尸持有了还能继续回调。我踩过这些坑,真的不想让你再踩一次。

一、CameraX的四大核心组件

CameraX把相机功能拆分成了几个独立的“用例”(UseCase)。每个用例负责一件事,逻辑清晰。我们直接看这张图:

CameraX核心组件与生命周期绑定流程 Android生命周期 LifecycleOwner onCreate → onStart → onResume onPause → onStop → onDestroy CameraX 核心层 ProcessCameraProvider bindToLifecycle(lifecycleOwner) Preview ImageCapture ImageAnalysis VideoCapture CameraX 自动管理 onResume → 自动打开相机,恢复预览 onPause → 自动关闭相机,释放Surface 关键点:bindToLifecycle 只绑定一次 重复 bind 会导致旧实例泄漏 配合 LifecycleOwner 实现自动释放,无需手动 close

四个核心用例:Preview、ImageCapture、ImageAnalysis、VideoCapture。它们都继承自 UseCase 基类。而 ProcessCameraProvider 是它们的“宿主”,负责把它们绑定给 LifecycleOwner。

关键认知:
不要手动调用 camera.close()。CameraX 在 LifecycleOwner 进入销毁状态时会自动释放所有用例的资源。你手动调用反而可能破坏内部状态机。

二、bindToLifecycle:一次绑定,终身受管

这是最容易被误用的API。我见过太多人每次进入页面都重新 bind 一次,却忘了 unbind 旧的实例。结果就是多个 UseCase 实例同时持有相机,内存暴涨,而且相机设备被锁定,再也打不开。

正确的做法:在 onCreate 或 onStart 中只 bind 一次。如果配置发生变化(比如旋转屏幕),CameraX 自动处理重建,你不需要重新 bind。

// ✅ 正确示范:只绑定一次
class CameraFragment : Fragment() {
    private var cameraProvider: ProcessCameraProvider? = null
    private var imageAnalysis: ImageAnalysis? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
        cameraProviderFuture.addListener({
            cameraProvider = cameraProviderFuture.get()
            bindUseCases() // 只在这里调用一次
        }, ContextCompat.getMainExecutor(requireContext()))
    }

    private fun bindUseCases() {
        val provider = cameraProvider ?: return
        
        // 必须先unbind所有旧的用例(如果存在)
        provider.unbindAll()
        
        val preview = Preview.Builder().build()
        val imageCapture = ImageCapture.Builder().build()
        imageAnalysis = ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build()
        
        provider.bindToLifecycle(
            viewLifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview, imageCapture, imageAnalysis
        )
    }

    override fun onDestroyView() {
        super.onDestroyView()
        // 不需要手动unbind!viewLifecycleOwner变成DESTROYED状态后CameraX自动释放
    }
}

⚠️ 避坑指南

我曾经在一个项目里用了 this(Fragment本身)作为 LifecycleOwner,结果 Fragment 重建时新实例绑定旧实例的 Lifecycle,导致旧相机资源永远无法释放。一定要使用 viewLifecycleOwner,它是 Fragment视图专用生命周期。

三、各用例的资源泄漏风险点

每个用例都有自己特有的泄漏场景。我总结了一个表格,方便你对照排查:

核心组件 泄漏风险点 检查方法
Preview 未释放 SurfaceTexture
预览 Surface 被长时间持有
onPause 后是否回调 onSurfaceDestroyed
ImageCapture 拍照回调未释放 ImageProxy
OnImageCapturedCallback 持有 Activity
检查 ImageProxy.close() 是否在 finally 块中
ImageAnalysis 每帧 ImageProxy 未 close
分析线程泄漏导致内存堆积
确保 analyze() 中每个分支都调用了 image.close()
VideoCapture MediaRecorder 未释放
录像文件句柄泄漏
检查 stopRecording 之后 recorder.release()

四、生命周期回调与资源时序

你想想看,CameraX 是怎么知道什么时候该关闭相机的?其实它监听了 Lifecycle 的 ON_PAUSE 和 ON_DESTROY 事件。内部维护了一个状态机:

  1. ON_RESUME:打开相机、配置捕获会话、启动预览。
  2. ON_PAUSE:关闭相机、释放会话、释放 Surface。
  3. ON_DESTROY:清理所有 UseCase 内部缓存,释放 Native 内存。

但有一个容易忽略的细节:viewLifecycleOwner 的生命周期比 this 的生命周期多了一个 ON_DESTROY_VIEW 状态。如果你用错了,CameraX 可能会在视图销毁后还继续持有 Surface,导致 SurfaceTexture 泄漏。

💡 个人经验

我在做扫码相机时,因为用了 lifecycleScope 在 Fragment 的 onCreate 中启动分析协程,结果 Fragment 进入后台后再回来,协程内部还持有旧的 ImageProxy。解决方法:把协程的生命周期绑定到 viewLifecycleOwner.lifecycleScope,前台的协程随着视图销毁自动取消。

五、如何验证生命周期绑定是否生效?

光靠看代码不放心,咱们得拿证据说话。我推荐用 adb shell dumpsys media.camera 命令来查看相机设备是否被正确释放:

# 在 onPause 之后检查相机设备列表
adb shell dumpsys media.camera | grep -E "Camera [0-9]+" 
# 如果还有 "Camera 0: Opened" 并且没在后台使用,说明泄漏了

# 更精细的检查:查看每个进程打开的相机设备
adb shell dumpsys media.camera | grep "Client"  
# 正常情况 onPause 后应该没有任何 Client 信息

同时,在 Android Studio 的 Memory Profiler 里观察 ImageProxy 和 SurfaceTexture 的实例数。如果退出页面后它们的数量不归零,说明资源泄漏了。

好,关于核心组件与生命周期绑定,这些就是我最想告诉你的实战经验。下一部分我们要深入具体用例的资源泄漏排查——先拿 ImageAnalysis 开刀,因为它是最容易出内存问题的环节。