1、CameraX 概述与预览流程简介
做 Android 相机开发的朋友应该都经历过那段「每个厂商各有一套,适配到崩溃」的日子。我在2020年接手一个多品牌平板项目时,光预览启动就适配了六家厂商——每家对 Camera2 API 的实现细节都不一样。直到 Google 推出了 CameraX,我才算真正喘了口气。
说白了,CameraX 就是在 Camera2 之上又封了一层。它帮你把那些烦人的厂商差异、生命周期管理、旋转角度、预览输出格式等琐事都处理好了。你只需关心业务逻辑,比如拍照、录制、分析。
1.1 CameraX 的核心定位
CameraX 不是一个独立的新相机系统,而是 Jetpack 的一部分。它和 Lifecycle、ViewModel 这些组件一样,主打「简化」和「兼容」。我个人的理解是:它把 Camera2 那种「全量 API」变成了「用例式 API」。
什么叫用例式?你看看 CameraX 的几个核心用例就知道了:
- Preview —— 预览
- ImageCapture —— 拍照
- ImageAnalysis —— 图像分析(比如扫码、人脸检测)
- VideoCapture —— 视频录制(1.3 之后加入)
每个用例都封装好了最佳实践。你不需要自己处理 Surface 的配置、不需要操心旋转、不需要写一堆回调来适配不同尺寸——这些 CameraX 都帮你做了。
📌 关键数据:CameraX 在 Android 5.0 (API 21) 以上的设备上覆盖率超过 99%。这是通过一个叫「CameraXConfig」的兼容性配置层实现的,底层自动切换到 Camera2 或 Camera1。
1.2 预览启动流程的全景
我们这门课的主题是「预览启动耗时拆解」,所以必须先搞清楚:从你调用 ProcessCameraProvider.getInstance() 到第一帧画面显示,中间到底经历了什么?
我曾在小米10上打过一次详细 log,把整个流程拆成了下面这几个阶段:
- 初始化 CameraX 提供者 —— 加载底层库、初始化 CameraX 内部服务
- 绑定生命周期 —— 将 Preview 用例绑定到 LifecycleOwner
- 创建相机实例 —— 打开相机设备、配置会话
- 配置预览输出 —— 设置 SurfaceProvider、输出尺寸、旋转角度
- 等待第一帧 —— 从 Surface 回调到显示到 TextureView 或 SurfaceView
等一下,你可能觉得这还没完。实际项目中,这五步之间还会穿插一些异步等待、多次 bind、甚至线程切换。我建议你把下面这张图存下来,它涵盖了预览启动的核心节点:
💡 提示:虚线箭头表示这两个阶段是异步并行的。
我在三星 S20 上测过,绑定生命周期后 CameraX 几乎同时发起打开相机和配置 Surface 的请求,但第一部分(初始化提供者)往往最容易被忽略——它里面包含了一个「寻找可用相机」的操作,慢的时候能占到总耗时的 30%。
1.3 每一步在做什么?
我们来逐一拆解每个阶段,先有个整体印象,后面章节会深入每一个环节的细节。
| 阶段 | 主要工作 | 常见耗时原因 |
|---|---|---|
| 1. 初始化提供者 | 加载 CameraX 内部组件,扫描系统中可用的摄像头 | 厂商 HAL 初始化慢、多摄像头扫描耗时 |
| 2. 绑定生命周期 | 将 Preview 用例注册到当前 Activity 的 Lifecycle | 多次 bind/unbind 导致状态重置 |
| 3. 创建相机 | 调用 Camera2 API 的 openCamera() 和 createCaptureSession() | 相机设备被其他应用占用、权限未授权 |
| 4. 配置预览输出 | 设置 Surface 尺寸、旋转、分辨率 | 尺寸匹配失败降级、TextureView 初始化 |
| 5. 第一帧到达 | 等待 Camera 输出第一帧到 Surface | 底层帧率未稳定、 Surface 未就绪 |
1.4 一个简单的代码骨架
先给你看一段最基础的 CameraX 预览启动代码。记住,这只是骨架,我们后续课程会逐行优化它。
// 1. 获取 CameraX 提供者
val provider = ProcessCameraProvider.getInstance(context).await()
// 2. 创建 Preview 用例
val preview = Preview.Builder()
.setTargetResolution(Size(1920, 1080))
.build()
// 3. 设置预览 Surface
val surfaceProvider = previewView.surfaceProvider
preview.setSurfaceProvider(surfaceProvider)
// 4. 选择相机(后置/前置)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// 5. 绑定到生命周期
provider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview
)
你发现没有?这五步里,bindToLifecycle() 是阻塞的——它会等到相机打开、会话创建完成、第一帧准备好才返回。也就是说,启动耗时就藏在这五行代码里。
⚠️ 注意:上面代码中 ProcessCameraProvider.getInstance(context).await() 是一个 suspend 函数,不能在主线程直接调用,否则会 ANR。我曾经在一个旧项目里看到同事直接在主线程 .await(),结果闪退了——这个问题我们后面会专门讲如何优雅处理。
1.5 为什么需要拆解耗时
你可能觉得预览启动快不快无所谓。但想想看:
- 扫码场景:用户举起手机扫码,预览黑屏超过 1 秒,体验极差
- 扫文档/拍照:每次进入相机都要等,用户会骂
- 多摄像头切换:如果切摄像头需要 2 秒,基本废了
我经历过的极限场景是在一个工业平板项目上,初始预览启动花了 3.2 秒,客户直接拒绝验收。后来我逐个阶段打 log,发现初始化的 CameraX 提供者里有个「枚举相机」操作,在部分 HAL 实现上会串行查询所有摄像头信息——那个版本有 4 个摄像头,每查一个要 200ms。优化方案很简单:缓存相机列表,只查一次。最终降到了 0.8 秒。
所以,拆解是第一步。只有知道每个阶段花了多少时间,你才能精准下刀。
1.6 本章小结
我们已经知道了:
- CameraX 是向 Camera2 的简化封装
- 预览启动要经历初始化提供者、绑定生命周期、创建相机、配置输出、等待第一帧五个核心阶段
- 每个阶段都有对应的优化切入点
接下来,我会带你深入第一帧的每一个节点,结合代码、log 和实战踩坑记录,把启动耗时一帧一帧抠出来。嗯,准备好了吗?