2、Camera软件架构总览:Android Camera HAL架构、Linux V4L2框架、用户空间与内核空间交互

这一章,我们来聊聊Camera软件的整体骨架。说白了,就是搞清楚摄像头数据从硬件到应用,到底是怎么一路“跑”上来的。

我刚开始接触Camera驱动时,最头疼的就是搞不清用户空间和内核空间的分界线。代码看着看着就迷路了。后来带我的老工程师丢给我一句话:“你只要记住,内核只管干活,用户只管发指令。” 嗯,这句话帮我理清了很多东西。

2.1 整体分层:谁在什么位置?

一个典型的Android Camera软件栈,从上到下大概分四层:

  • 应用层:相机App,调用Android SDK的Camera2 API。
  • Framework层:Android的CameraService、CameraProvider,负责调度和策略。
  • HAL层:硬件抽象层,厂商实现,直接操作硬件。
  • 内核层:Linux Kernel中的V4L2驱动、Sensor驱动、ISP驱动。

你想想看,App按一下快门,这条指令要穿过这么多层,每一层都有它的职责。哪一层出了问题,相机就可能打不开、卡顿、或者出图偏色。

核心观点:HAL层是“翻译官”,V4L2是“邮递员”。HAL把Android的请求翻译成硬件能懂的命令,V4L2负责在用户空间和内核空间之间传递这些命令和数据。

Camera 软件架构分层图 应用层 (Camera App) Framework 层 (CameraService) HAL 层 (Camera HAL) 内核层 (V4L2 + Sensor/ISP Driver) 硬件层 (Camera Sensor / ISP) 数据流向 App 发起请求 Framework 调度 HAL 转换命令 V4L2 传递 驱动控制硬件 图像数据返回 逐层回调

2.2 Android Camera HAL 架构

HAL,全称 Hardware Abstraction Layer。它的存在,就是为了让Android Framework不用关心你用的是哪家的Sensor、哪家的ISP。

我个人习惯把HAL理解成一个“插件”。Google定义好接口,厂商去实现。只要接口对得上,Framework就能正常工作。

2.2.1 HAL 的两种形态

形态 说明 典型场景
HAL1 旧版,接口简单,但灵活性差。预览和拍照的流程是固定的。 Android 5.0 之前的设备,低端平台。
HAL3 新版,基于Camera3 API,支持Pipeline模型。每个请求可以独立配置。 Android 5.0 之后的主流设备,支持HDR、多帧合成。

注意: 现在新项目基本不会再用HAL1了。如果你还在维护老平台,建议尽快迁移到HAL3。我曾经在一个项目上因为HAL1的局限性,导致无法实现“拍照后快速切换滤镜”的需求,最后不得不大改底层。

2.2.2 HAL3 的核心组件

HAL3里,有几个关键角色你必须记住:

  • CameraDevice:代表一个物理摄像头。打开摄像头就是打开这个设备。
  • CameraStream:数据流。预览流、拍照流、视频流,都是它。
  • CaptureRequest:一次捕获请求。里面包含了你要的配置:分辨率、帧率、对焦位置、AE补偿等等。
  • CaptureResult:捕获结果。包含图像数据和当时的元数据(比如实际曝光时间、ISO)。

说白了,HAL3的工作流程就是:Framework发一个 CaptureRequest 下来,HAL处理完,返回一个 CaptureResult。就这么简单。但实现起来,里面的坑可不少。

2.3 Linux V4L2 框架

V4L2,全称 Video for Linux 2。它是Linux内核里处理视频设备的统一框架。Camera Sensor在Linux下,就是作为一个V4L2设备存在的。

我刚开始学V4L2时,被它那一堆ioctl命令搞得晕头转向。后来我总结了一个规律:V4L2的核心就是“打开设备 -> 设置格式 -> 申请缓冲区 -> 入队/出队 -> 关闭设备”。

2.3.1 V4L2 设备节点

在Linux文件系统里,Camera设备通常出现在 /dev/video0、/dev/video1 这样的节点上。你通过 open() 系统调用打开它,然后通过 ioctl() 和它沟通。

小技巧: 用 v4l2-ctl --list-devices 命令可以快速查看当前系统有哪些V4L2设备。我在调试时,第一步永远是跑这个命令,确认设备节点存在。

2.3.2 V4L2 核心流程

一个典型的V4L2采集流程,大概分这几步:

  1. 打开设备:fd = open("/dev/video0", O_RDWR)。
  2. 查询能力:ioctl(fd, VIDIOC_QUERYCAP, &cap),看看设备支持什么。
  3. 设置格式:ioctl(fd, VIDIOC_S_FMT, &fmt),告诉驱动你要的分辨率和像素格式(比如YUYV、MJPEG)。
  4. 请求缓冲区:ioctl(fd, VIDIOC_REQBUFS, &req),申请一组用于存放图像数据的内存。
  5. 映射/绑定缓冲区:mmap() 或者 dmabuf,让用户空间能访问这些内存。
  6. 入队:ioctl(fd, VIDIOC_QBUF, &buf),把空缓冲区交给驱动。
  7. 开始流:ioctl(fd, VIDIOC_STREAMON, &type),驱动开始采集数据。
  8. 出队:ioctl(fd, VIDIOC_DQBUF, &buf),从驱动取回填满数据的缓冲区。
  9. 处理数据:用户空间处理图像。
  10. 停止流:ioctl(fd, VIDIOC_STREAMOFF, &type),关闭设备。
// 伪代码示例:V4L2 采集一帧
int fd = open("/dev/video0", O_RDWR);
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
ioctl(fd, VIDIOC_S_FMT, &fmt);

struct v4l2_requestbuffers req = {0};
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);

// ... 映射缓冲区,入队 ...

ioctl(fd, VIDIOC_STREAMON, &type);

struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf);  // 阻塞等待一帧

// 处理 buf 中的数据

ioctl(fd, VIDIOC_QBUF, &buf);   // 重新入队

ioctl(fd, VIDIOC_STREAMOFF, &type);
close(fd);

2.4 用户空间与内核空间交互

这是整个Camera软件栈里最核心的“桥梁”。用户空间的App不能直接操作硬件,必须通过内核空间的驱动。那它们怎么通信呢?

主要有三种方式:

方式 描述 性能 典型用途
系统调用 (ioctl) 用户空间通过 ioctl() 向驱动发送命令和配置。 中等 设置格式、启动/停止流、查询能力。
内存映射 (mmap) 将内核空间的缓冲区映射到用户空间,避免数据拷贝。 高 传输图像数据(预览、拍照、视频)。
DMA-BUF 更现代的缓冲区共享机制,允许不同设备间共享内存。 极高 ISP直接输出到GPU/Display,零拷贝。

关键点: 图像数据是“大头”。一帧1080p的YUV数据大约3MB,30fps就是90MB/s。如果每次都用 read() 从内核拷贝到用户空间,CPU开销会非常大。所以,mmap 和 DMA-BUF 几乎是所有高性能Camera方案的标配。

我记得有一次,客户反馈相机预览卡顿。我排查了半天,发现是HAL层用了 read() 来读取V4L2缓冲区,导致CPU占用率飙升。改成 mmap 后,问题立刻解决。嗯,这种“低级错误”在赶项目时很容易犯,大家一定要留意。

2.5 数据流全景:从App到Sensor

我们把所有东西串起来,看一个完整的“按下快门”数据流:

  1. App 调用 CameraCaptureSession.capture()。
  2. Framework 的 CameraService 收到请求,把它封装成一个 CaptureRequest,发给 HAL。
  3. HAL 收到请求后,解析参数,然后通过 ioctl() 把配置下发给 V4L2 驱动。
  4. V4L2 驱动 配置 Sensor 和 ISP 寄存器,启动一次曝光。
  5. Sensor 曝光结束,输出 RAW 数据给 ISP。
  6. ISP 处理完数据,输出 YUV/JPEG 到 V4L2 的 DMA 缓冲区。
  7. HAL 通过 DQBUF 拿到填满数据的缓冲区(通过 mmap 或 DMA-BUF 直接访问)。
  8. HAL 把图像数据和元数据打包成 CaptureResult,回调给 Framework。
  9. Framework 把结果传给 App,App 显示或保存。

你看,这一圈下来,每一层都在做自己分内的事。任何一个环节慢了,都会影响用户体验。

我的建议: 调试Camera性能问题时,先别急着看代码。先搞清楚数据流卡在哪一层。用 perf 或者 ftrace 抓一下内核事件,看看 DQBUF 的等待时间,基本就能定位问题。