CameraX 简介:设计理念、生命周期感知与四大用例

大家好,我是你们这节课的讲师。今天我们来聊聊 CameraX。

说实话,Android 的摄像头开发,在 CameraX 出来之前,确实有点「劝退」。我自己早年做项目时,光是处理摄像头旋转、生命周期、不同厂商的兼容性问题,就熬了好几个通宵。后来 Google 推出了 CameraX,我第一时间就试用了。嗯,怎么说呢,它把很多脏活累活都包了,让我能更专注于业务逻辑。

这一章,我们就来拆解 CameraX 的核心。我会从设计理念讲起,然后带你理解它的生命周期感知,最后把四个核心用例(Preview、ImageCapture、VideoCapture、ImageAnalysis)逐个说清楚。

一、CameraX 的设计理念:让摄像头开发变简单

CameraX 的设计初衷,说白了就是一句话:降低复杂度,提高一致性

你想想看,以前用 Camera2 API 写一个拍照功能,要处理多少东西?

  • 要自己管理 CameraManager、CameraDevice、CameraCaptureSession
  • 要处理各种 Surface 的创建和销毁
  • 要适配不同厂商的奇葩行为

CameraX 把这些都封装起来了。它采用了一种「用例(Use Case)」驱动的架构。什么意思呢?就是你只需要告诉 CameraX:「我要预览」、「我要拍照」、「我要录像」,它就会自动帮你配置好底层的 Camera2 流程。

核心设计理念:

  • 易用性:几行代码就能实现预览和拍照
  • 生命周期感知:自动绑定 Activity/Fragment 的生命周期
  • 设备兼容性:Google 做了大量兼容性测试,覆盖了市面上绝大多数设备
  • 可扩展性:支持自定义扩展,比如美颜、HDR

我个人习惯把 CameraX 比作一个「智能管家」。你告诉它你要做什么,它就去协调底层硬件,你只需要关注结果就行。

二、生命周期感知:再也不用担心 Activity 销毁了

这是 CameraX 最让我喜欢的一个特性。以前用 Camera2,我必须在 onResume() 里打开摄像头,在 onPause() 里关闭,稍有不慎就会导致内存泄漏或者摄像头被占用。

CameraX 通过 ProcessCameraProviderLifecycleOwner 实现了自动绑定。你只需要把 Activity 或 Fragment 的生命周期传给它,它就会自动在合适的时机打开和关闭摄像头。

举个例子:

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

cameraProviderFuture.addListener({
    val cameraProvider = cameraProviderFuture.get()
    
    // 绑定生命周期
    val camera = cameraProvider.bindToLifecycle(
        this,          // LifecycleOwner
        cameraSelector,
        previewUseCase,
        imageCaptureUseCase
    )
}, ContextCompat.getMainExecutor(this))

看到了吗?bindToLifecycle 方法把摄像头和当前页面的生命周期绑定了。当页面销毁时,CameraX 会自动释放摄像头资源。我曾经在一个项目里忘记释放 Camera2 的 session,结果导致其他应用无法调用摄像头,用户直接给了差评。用 CameraX 之后,这种低级错误再也没犯过。

小提示: 如果你在 Fragment 中使用 CameraX,记得把 LifecycleOwner 传成 viewLifecycleOwner,而不是 Fragment 本身。这样可以避免 Fragment 视图销毁时摄像头还占用着。

三、四大用例详解

CameraX 提供了四个开箱即用的用例。它们可以单独使用,也可以组合使用。下面我逐个介绍。

1. Preview(预览)

预览是最基本的功能。CameraX 的 Preview 用例需要配合 PreviewView 使用。你只需要创建一个 Preview 对象,设置一个 SurfaceProvider,然后绑定到生命周期即可。

val preview = Preview.Builder()
    .build()

preview.setSurfaceProvider(previewView.surfaceProvider)

cameraProvider.bindToLifecycle(
    this,
    cameraSelector,
    preview
)

这里要注意一点:PreviewView 是一个自定义的 View,它内部处理了 Surface 的创建和显示。你不需要再像以前那样自己写 TextureView 或 SurfaceView 了。

2. ImageCapture(拍照)

拍照用例也很简单。你创建一个 ImageCapture 对象,然后调用它的 takePicture() 方法。

val imageCapture = ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
    .build()

// 拍照
imageCapture.takePicture(
    context,
    executor,
    object : ImageCapture.OnImageSavedCallback {
        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
            // 照片保存成功
        }

        override fun onError(exception: ImageCaptureException) {
            // 处理错误
        }
    }
)

我建议你在设置 CaptureMode 时,根据场景选择:

  • CAPTURE_MODE_MINIMIZE_LATENCY:优先保证快门速度,适合抓拍
  • CAPTURE_MODE_MAXIMIZE_QUALITY:优先保证画质,适合静物拍摄

避坑指南: 我曾经在低端机上使用 MAXIMIZE_QUALITY 模式,结果拍照后处理时间过长,导致 ANR。后来我改成 MINIMIZE_LATENCY,并在后台异步保存图片,问题就解决了。所以,如果你的应用需要快速响应,建议用延迟模式。

3. VideoCapture(录像)

录像用例稍微复杂一点,但 CameraX 也做了很好的封装。你需要创建一个 VideoCapture 对象,并设置一个 Recorder

val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
    .build()

val videoCapture = VideoCapture.Builder(recorder)
    .build()

// 开始录像
val mediaStoreOutput = MediaStoreOutputOptions.Builder(
    contentResolver,
    MediaStore.Video.Media.EXTERNAL_CONTENT_URI
)
    .build()

videoCapture.output.prepareRecording(context, mediaStoreOutput)
    .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
        // 录像状态回调
    }

// 停止录像
videoCapture.stopRecording()

这里有个细节:Recorder 是单独配置的,你可以设置视频质量、帧率等参数。我个人习惯把 Quality.HIGHEST 作为默认值,但如果用户存储空间不足,我会降级到 Quality.SD

4. ImageAnalysis(图像分析)

这个用例是给开发者做实时图像处理用的,比如二维码扫描、人脸检测。它会把每一帧图像数据回调给你。

val imageAnalysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

imageAnalysis.setAnalyzer(executor) { imageProxy ->
    // 处理每一帧图像
    val buffer = imageProxy.planes[0].buffer
    // ... 你的分析逻辑

    // 处理完后一定要关闭
    imageProxy.close()
}

这里有一个非常重要的点:一定要调用 imageProxy.close()。否则 CameraX 会认为你还在处理上一帧,导致后续帧被阻塞。我见过不少新手在这里踩坑,图像分析越来越卡,最后直接崩溃。

关于背压策略:

  • STRATEGY_KEEP_ONLY_LATEST:只保留最新的一帧,适合实时性要求高的场景(比如扫码)
  • STRATEGY_BLOCK_PRODUCER:阻塞生产者,直到当前帧处理完毕,适合需要处理每一帧的场景(比如视频滤镜)

四、知识体系总览

为了让你更直观地理解 CameraX 的整体架构,我画了一张图。你可以看到,ProcessCameraProvider 是核心,它管理着生命周期和所有用例。

CameraX 架构总览 ProcessCameraProvider LifecycleOwner Preview ImageCapture VideoCapture ImageAnalysis 底层 Camera2 API(自动管理) CameraX 自动处理生命周期、设备兼容性、线程切换

从这张图你可以看到,ProcessCameraProvider 是中心枢纽。它接收 LifecycleOwner 的生命周期信号,然后协调四个用例的工作。底层虽然还是 Camera2,但开发者完全不需要关心那些细节。

五、组合使用:一个完整的例子

在实际项目中,我们通常会把 Preview 和 ImageCapture 组合在一起。比如做一个扫码应用,可能还需要加上 ImageAnalysis。下面是一个同时使用 Preview 和 ImageCapture 的代码片段:

class CameraActivity : AppCompatActivity() {
    private lateinit var previewView: PreviewView
    private lateinit var imageCapture: ImageCapture

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)
        previewView = findViewById(R.id.previewView)

        startCamera()
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            // 创建预览用例
            val preview = Preview.Builder()
                .build()
                .also { it.setSurfaceProvider(previewView.surfaceProvider) }

            // 创建拍照用例
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

            // 选择后置摄像头
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // 绑定到生命周期
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture
                )
            } catch (e: Exception) {
                Log.e("CameraX", "绑定失败", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    fun takePhoto() {
        val photoFile = File(
            externalMediaDirs.first(),
            "${System.currentTimeMillis()}.jpg"
        )

        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    Log.d("CameraX", "照片保存成功: ${photoFile.absolutePath}")
                }

                override fun onError(exception: ImageCaptureException) {
                    Log.e("CameraX", "拍照失败", exception)
                }
            }
        )
    }
}

这段代码里,我同时绑定了 Preview 和 ImageCapture 两个用例。当用户点击拍照按钮时,调用 takePhoto() 方法即可。整个过程不需要手动打开或关闭摄像头,CameraX 会自动处理。

总结一下 CameraX 的优势:

  • 代码量减少 60% 以上
  • 自动处理生命周期,避免内存泄漏
  • 内置设备兼容性解决方案
  • 用例可自由组合,灵活度高

好了,这一章的内容就到这里。CameraX 的设计理念和四个用例,你掌握了吗?下一章我们会深入每个用例的配置细节,包括分辨率选择、闪光灯控制、对焦模式等。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。


公众号:蓝海资料掘金营,微信deep3321