深入 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)
}
)
}
PreviewView 的 surfaceProvider 是连接 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,但它的内部转换只处理了 PreviewView 的 displayOrientation,不处理前置摄像头的镜像。人像模式下面部对焦的坐标偏移大概率需要自行校正。
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% 的应用场景根本不需要那种灵活性。