CameraX 相机开发:从生命周期到 AI 集成的完整实践

大家好,今天我们来聊聊 CameraX。说实话,Android 相机开发曾经是个让人头疼的领域。我记得刚入行那会儿,为了写一个简单的拍照功能,得跟 Camera1 的 SurfaceView、各种回调、还有不同厂商的兼容性问题死磕。后来 Google 推出了 CameraX,嗯,这玩意儿确实让事情变得简单多了。

CameraX 的核心思想是什么?说白了,就是帮你把相机生命周期和 Android 组件生命周期绑定在一起。你不用再手动管理相机的打开、关闭、释放这些琐事。你只需要关注业务逻辑——拍照、预览、分析图像。剩下的,CameraX 帮你搞定。

核心要点:CameraX 是一个 Jetpack 组件,它通过 ProcessCameraProvider 与 LifecycleOwner 绑定,自动处理相机的生命周期。你不需要再写 onResume 里打开相机、onPause 里释放相机这种模板代码。

生命周期绑定:让相机跟着 Activity/Fragment 走

我们先来看一个最简单的例子。假设你要在 Activity 里集成相机预览,传统做法你得写多少代码?我算过,至少 200 行。用 CameraX 呢?

// 1. 获取 ProcessCameraProvider
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

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

    // 2. 创建 Preview 用例
    val preview = Preview.Builder().build()
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

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

看到没?核心就三步:获取 Provider、创建用例、绑定生命周期。bindToLifecycle 这个方法一调用,CameraX 就会自动监听 Activity 的 onStart、onStop、onDestroy 事件。你 Activity 销毁了,相机资源自动释放。我曾经在项目里见过有人忘了释放相机导致内存泄漏,用 CameraX 之后这种问题再也没出现过。

个人习惯:我一般会在 Application 初始化时预加载 ProcessCameraProvider,这样进入相机页面时能快 200-300ms。不过要注意,预加载会增加内存占用,权衡一下。

三大核心用例:Preview、ImageCapture、ImageAnalysis

CameraX 提供了三个开箱即用的用例。你可以单独用,也可以组合用。我画了一张图,帮你理清它们的关系:

CameraX 核心架构 ProcessCameraProvider + LifecycleOwner Preview SurfaceProvider 预览画面 ImageCapture 拍照并保存图片 ImageAnalysis 逐帧分析图像数据 HDR / 夜景模式 ML Kit 集成 相机配置扩展

Preview 用例:让用户看到画面

Preview 用例负责把相机画面渲染到屏幕上。你需要提供一个 PreviewView(CameraX 提供的专用 View),然后通过 setSurfaceProvider 关联起来。

// 布局文件中的 PreviewView
// <androidx.camera.view.PreviewView
//     android:id="@+id/viewFinder"
//     android:layout_width="match_parent"
//     android:layout_height="match_parent" />

// 代码中绑定
val preview = Preview.Builder()
    .setTargetAspectRatio(AspectRatio.RATIO_16_9)
    .build()

preview.setSurfaceProvider(viewFinder.surfaceProvider)

这里有个坑,我踩过。setTargetAspectRatio 设置的是期望比例,不是强制比例。如果设备不支持 16:9,CameraX 会自动选一个最接近的。你想想看,如果你强制要求 4:3,有些全面屏手机画面会被拉伸,用户体验很差。所以我建议用 RATIO_16_9,兼容性最好。

ImageCapture 用例:拍照并保存

拍照功能,CameraX 封装得非常优雅。你只需要调用 takePicture 方法,传入一个 OnImageCapturedCallback 或者一个文件保存路径。

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

// 拍照到内存
imageCapture.takePicture(
    ContextCompat.getMainExecutor(this),
    object : ImageCapture.OnImageCapturedCallback() {
        override fun onCaptureSuccess(image: ImageProxy) {
            // 处理 image 对象
            // 注意:用完必须调用 image.close() 释放
        }
        override fun onError(exception: ImageCaptureException) {
            Log.e(TAG, "拍照失败: ${exception.imageCaptureError}")
        }
    }
)

// 拍照到文件
val photoFile = File(externalMediaDirs.first(), 
    "IMG_${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) {
            // 文件已保存
        }
        override fun onError(exception: ImageCaptureException) {
            // 处理错误
        }
    }
)

注意:ImageProxy 对象使用后必须调用 close(),否则会导致相机管道阻塞。我曾经在代码审查时发现有人忘了 close,结果拍几张照片后相机就卡死了。建议用 use 扩展函数自动释放。

ImageAnalysis 用例:逐帧分析

这个用例特别适合做实时图像处理,比如扫码、人脸检测、滤镜等。它会把每一帧图像数据通过 Analyzer 回调给你。

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

imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
    // imageProxy 是每一帧的图像数据
    val buffer = imageProxy.planes[0].buffer
    // 在这里做你的图像处理逻辑
    
    // 处理完后必须关闭
    imageProxy.close()
}

关于背压策略,我多说两句。STRATEGY_KEEP_ONLY_LATEST 会丢弃旧帧,只保留最新一帧。如果你的分析逻辑比较耗时,这个策略能保证你不会被帧数据淹没。STRATEGY_BLOCK_PRODUCER 则会阻塞生产者,直到你处理完当前帧。我个人习惯用 KEEP_ONLY_LATEST,因为实时性更重要,丢几帧无所谓。

相机配置与扩展:HDR、夜景模式

CameraX 通过 CameraXConfig 和 ExtensionsManager 来支持厂商特定的功能。比如 HDR 和夜景模式,这些功能依赖硬件和驱动支持。

// 检查并启用 HDR
val extensionsManager = ExtensionsManager.getInstance(context)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

if (extensionsManager.isExtensionAvailable(
        cameraProvider, 
        cameraSelector, 
        ExtensionMode.HDR
    )) {
    val hdrCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
        cameraSelector, 
        ExtensionMode.HDR
    )
    cameraProvider.bindToLifecycle(
        this, 
        hdrCameraSelector, 
        preview, 
        imageCapture
    )
}

// 夜景模式同理,把 ExtensionMode.HDR 换成 ExtensionMode.NIGHT

这里要注意,不是所有设备都支持这些扩展。我测试过,Pixel 系列支持得最好,一些国产中低端手机可能不支持。所以一定要先调用 isExtensionAvailable 检查,否则会 crash。

CameraX 与 ML Kit 集成:让相机拥有 AI 能力

ML Kit 是 Google 的机器学习套件,它和 CameraX 的 ImageAnalysis 用例是天作之合。你只需要把 ImageProxy 转换成 ML Kit 需要的 InputImage 格式,然后调用对应的检测器即可。

下面是一个实时人脸检测的例子:

// 创建人脸检测器
val options = FaceDetectorOptions.Builder()
    .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
    .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
    .build()
val faceDetector = FaceDetection.getClient(options)

// 在 ImageAnalysis.Analyzer 中集成
imageAnalysis.setAnalyzer(executor) { imageProxy ->
    @Suppress("UNCHECKED_CAST")
    val mediaImage = imageProxy.image
    if (mediaImage != null) {
        val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
        
        faceDetector.process(inputImage)
            .addOnSuccessListener { faces ->
                // 处理检测到的人脸
                for (face in faces) {
                    val bounds = face.boundingBox
                    val smileProb = face.smilingProbability
                    // 绘制人脸框、添加贴纸等
                }
            }
            .addOnCompleteListener {
                // 必须关闭 imageProxy
                imageProxy.close()
            }
    } else {
        imageProxy.close()
    }
}

ML Kit 支持的场景很多:条码扫描、文字识别、对象检测、图像标注等。我曾在电商项目里用 CameraX + ML Kit 做商品条码扫描,用户体验比传统 ZXing 库好很多,因为 CameraX 自动处理了旋转和缩放。

性能优化建议:ML Kit 的分析任务建议放在独立线程池,不要占用主线程。另外,如果检测器返回结果太慢,可以降低 ImageAnalysis 的分辨率,比如用 setTargetResolution(Size(640, 480))。分辨率越低,处理越快。

组合使用:一个完整的相机页面

最后,我们看看如何把三个用例组合在一起。实际项目中,你通常需要同时支持预览、拍照和分析。

val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
    .build()
val imageAnalysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

// 绑定所有用例
cameraProvider.bindToLifecycle(
    this,
    cameraSelector,
    preview,
    imageCapture,
    imageAnalysis
)

bindToLifecycle 方法可以接受多个用例。CameraX 会自动判断设备是否支持同时运行这些用例。如果不支持,它会抛出异常。我建议在 bind 之前先检查一下:

val available = cameraProvider.hasCamera(cameraSelector)
if (!available) {
    // 降级处理,比如只绑定 Preview 和 ImageCapture
}

好了,CameraX 的核心内容就这些。从生命周期绑定到三大用例,再到扩展功能和 ML Kit 集成,你会发现 CameraX 的设计思路就是「让复杂的事情变简单」。你不需要成为相机专家,也能写出稳定、高效的相机应用。嗯,这就是 Jetpack 组件存在的意义。


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