深入 Android CameraX 架构解析:从 Camera2 Pipeline 到 Compose 声明式相机的全链路实践

去年在一个视频社交应用中做相机模块重构,团队接手的是一个基于 Camera2 API 的手写方案。打开代码的瞬间我就意识到一件事:光 CameraCaptureSession 的状态机就占了 200 行,而且每次横竖屏切换都会随机触发几个 ANR。

Camera2 的痛点是结构性的——它把硬件能力完全暴露给开发者,但缺少合理的抽象层。CameraX 要解决的问题,就是在这条链路上补上缺失的那一环。

HAL 到 UseCase:一条管线的四层抽象

从底层硬件到应用层,整个相机管线分为四层:

HAL 层(Hardware Abstraction Layer):相机驱动与系统服务的接口。各家 SoC 厂商在这里实现 3A 算法(自动曝光、自动对焦、自动白平衡),CameraX 通过 Camera2 API 与 HAL 通信,不做任何 HAL 层的魔改。

Camera2 层:Android Framework 提供的底层 API。CameraManager 枚举设备,CameraDevice 代表物理摄像头,CameraCaptureSession 管理一次拍摄会话。问题就出在这里——Session 的创建、配置、状态切换全要手写,一旦配置错误就直接抛 CameraAccessException,很难排查。

UseCase 抽象层:这是 CameraX 的核心设计。它将相机操作抽象为四种 UseCase:

Preview        → Surface 实时预览
ImageCapture   → 拍照,输出 JPEG/RAW
VideoCapture   → 录像,输出编码视频流
ImageAnalysis  → 帧分析,输出 YUV/RGBA 给 ML 模型

每种 UseCase 独立配置,互不干扰。比如你在录像的同时做人脸检测,VideoCapture 和 ImageAnalysis 绑定同一个 LifecycleOwner,底层共享同一个 CameraCaptureSession,但各自拿到独立的 Surface

业务层:你的 Activity/Fragment/Composable,只跟 UseCase 打交道,感知不到 CameraDevice 和 Session 的存在。

这个分层的关键在于:CameraX 在内部维护了一个 Session 管理器。它会根据当前活跃的 UseCase 组合,自动拼装 OutputConfiguration 列表,合并冲突的 Stream,然后创建最优的 CaptureSession。你不再需要自己写 Session 的状态机。

生命周期绑定是怎么工作的

CameraX 的 ProcessCameraProvider 是整个框架的入口,通过 ListenableFuture 异步获取:

val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
    val cameraProvider = cameraProviderFuture.get()
    val preview = Preview.Builder().build()
    val imageCapture = ImageCapture.Builder()
        .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
        .build()
    // 绑定到当前生命周期
    cameraProvider.bindToLifecycle(
        lifecycleOwner, cameraSelector, preview, imageCapture
    )
}, ContextCompat.getMainExecutor(context))

bindToLifecycle 内部监听了 LifecycleOwner 的状态:

  • ON_START → 打开 CameraDevice,创建 Session,启动预览流
  • ON_STOP → 暂停但不释放硬件,切后台保持连接
  • ON_DESTROY → 关闭 Device,释放所有 Surface 和线程资源

这里有一个容易踩坑的点:UseCase 组合一旦绑定就不可动态修改。想在运行中切换拍照和录像,不能单独 unbind 某个 UseCase,只能全部解绑后重新绑定。官方的推荐做法是不需要切换时拆绑,而是同时绑定 Preview + ImageCapture + VideoCapture,通过业务逻辑控制哪个 UseCase 实际生效。多个 UseCase 共存时,CameraX 内部会做 Stream 共享,性能开销很小。

Compose 声明式相机:把 Surface 变成 Composable

在 Compose 中使用 CameraX,核心是解决 Surface 的声明式管理。传统的 AndroidView 桥接方式:

@Composable
fun CameraPreview(
    preview: Preview,
    modifier: Modifier = Modifier
) {
    AndroidView(
        factory = { ctx ->
            PreviewView(ctx).apply {
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                // SurfaceProvider 会自动对接 Preview UseCase
            }
        },
        modifier = modifier,
        update = { view ->
            preview.setSurfaceProvider(view.surfaceProvider)
        }
    )
}

PreviewViewsurfaceProvider 是连接 Compose 和 CameraX 的桥梁。它本质上是一个 Preview.SurfaceProvider 接口实现,CameraX 通过回调向它提供 SurfaceRequest

两个关键配置:

  • ImplementationMode.PERFORMANCE:使用 SurfaceView,渲染在独立窗口层,延迟更低但不支持 Compose 的 Modifier 动画和裁剪
  • ImplementationMode.COMPATIBLE:使用 TextureView,作为普通 View 参与 Compose 布局,兼容所有 Modifier 操作但多一次纹理拷贝

我一般用 COMPATIBLE 模式。现代机型的纹理拷贝开销基本可以忽略,但失去 Modifier 的灵活性在复杂 UI 里代价很大。

拍照逻辑封装成 Composable 也很自然:

@Composable
fun rememberImageCapture(): ImageCapture {
    return remember { ImageCapture.Builder().build() }
}

// 使用
val imageCapture = rememberImageCapture()
Button(onClick = {
    imageCapture.takePicture(
        ContextCompat.getMainExecutor(context),
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                // 处理保存结果
            }
            override fun onError(exc: ImageCaptureException) {
                // ImageCaptureException 包装了底层错误,比 Camera2 友好太多
            }
        }
    )
})

在 Compose 里集成 CameraX 还有一些现实问题。官方至今没有提供完整的 Compose Camera 封装库,PreviewView 还是得通过 AndroidView 桥接。这意味着复杂的取景框动画(缩放裁剪框、实时滤镜叠加)在 AndroidView 边界上会有渲染链路损耗。

生产环境踩过的坑

1. 对焦和测光的坐标系陷阱

Camera2 的 MeteringRectangle 使用传感器坐标系,而 PreviewView 的触摸坐标是显示坐标系。两者之间可能差一个旋转和镜像。CameraX 封装了 FocusMeteringAction,但它的内部转换只处理了 PreviewViewdisplayOrientation,不处理前置摄像头的镜像。人像模式下面部对焦的坐标偏移大概率需要自行校正。

2. 录像分辨率与预览分辨率不匹配

同时绑定 Preview 和 VideoCapture 时,如果 Preview 的目标分辨率是 1080p 而 VideoCapture 是 4K,HAL 层需要同时输出两路不同分辨率的 Stream,部分低端机型直接报 StreamConfigurationMap 异常。方案是把 Preview 的分辨率通过 setTargetResolution 与录像对齐,或者录像时降级预览。

3. ImageAnalysis 的内存泄漏

ImageProxy 对象必须手动 close()。CameraX 内部维护了一个有限的 Image 缓冲池(默认 2 帧),不 close 会造成帧丢失,分析回调越来越慢。我的习惯:

imageAnalysis.setAnalyzer(executor) { imageProxy ->
    try {
        // 处理帧数据
    } finally {
        imageProxy.close() // 必须
    }
}

4. 不要用 CameraX 做自定义 Camera 特效

如果你的需求是 AR 滤镜、自研美颜、实时风格迁移这类高度自定义的渲染管线,CameraX 的 UseCase 抽象反而成为障碍。这种情况直接用 Camera2 + OpenGL ES SurfaceTexture,自己管理渲染循环。CameraX 的设计目标是一般拍照和录像场景的生产力工具,不是特效引擎。

CameraX 的价值在于把相机开发中最容易出错的三件事自动化了:Session 管理、生命周期绑定和多 Stream 协调。代价是牺牲了部分灵活性——但 90% 的应用场景根本不需要那种灵活性。