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,把整个流程拆成了下面这几个阶段:

  1. 初始化 CameraX 提供者 —— 加载底层库、初始化 CameraX 内部服务
  2. 绑定生命周期 —— 将 Preview 用例绑定到 LifecycleOwner
  3. 创建相机实例 —— 打开相机设备、配置会话
  4. 配置预览输出 —— 设置 SurfaceProvider、输出尺寸、旋转角度
  5. 等待第一帧 —— 从 Surface 回调到显示到 TextureView 或 SurfaceView

等一下,你可能觉得这还没完。实际项目中,这五步之间还会穿插一些异步等待、多次 bind、甚至线程切换。我建议你把下面这张图存下来,它涵盖了预览启动的核心节点:

CameraX 预览启动核心流程 1. 初始化 ProcessCameraProvider 2. 绑定生命周期 bindToLifecycle() 3. 创建相机 Camera2打开 + 会话 4. 配置预览输出 Surface尺寸/旋转 5. 第一帧到达 onFrameAvailable 6. 预览显示 用户可见 注:绑定生命周期后创建相机与配置输出是异步并行的(虚线),最终等待第一帧汇合

💡 提示:虚线箭头表示这两个阶段是异步并行的。

我在三星 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 和实战踩坑记录,把启动耗时一帧一帧抠出来。嗯,准备好了吗?