2. Camera软件架构总览
这一章,我们来聊聊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负责在用户空间和内核空间之间传递这些命令和数据。
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采集流程,大概分这几步:
- 打开设备:
fd = open("/dev/video0", O_RDWR)。 - 查询能力:
ioctl(fd, VIDIOC_QUERYCAP, &cap),看看设备支持什么。 - 设置格式:
ioctl(fd, VIDIOC_S_FMT, &fmt),告诉驱动你要的分辨率和像素格式(比如YUYV、MJPEG)。 - 请求缓冲区:
ioctl(fd, VIDIOC_REQBUFS, &req),申请一组用于存放图像数据的内存。 - 映射/绑定缓冲区:
mmap()或者dmabuf,让用户空间能访问这些内存。 - 入队:
ioctl(fd, VIDIOC_QBUF, &buf),把空缓冲区交给驱动。 - 开始流:
ioctl(fd, VIDIOC_STREAMON, &type),驱动开始采集数据。 - 出队:
ioctl(fd, VIDIOC_DQBUF, &buf),从驱动取回填满数据的缓冲区。 - 处理数据:用户空间处理图像。
- 停止流:
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
我们把所有东西串起来,看一个完整的“按下快门”数据流:
- App 调用
CameraCaptureSession.capture()。 - Framework 的 CameraService 收到请求,把它封装成一个
CaptureRequest,发给 HAL。 - HAL 收到请求后,解析参数,然后通过
ioctl()把配置下发给 V4L2 驱动。 - V4L2 驱动 配置 Sensor 和 ISP 寄存器,启动一次曝光。
- Sensor 曝光结束,输出 RAW 数据给 ISP。
- ISP 处理完数据,输出 YUV/JPEG 到 V4L2 的 DMA 缓冲区。
- HAL 通过
DQBUF拿到填满数据的缓冲区(通过mmap或DMA-BUF直接访问)。 - HAL 把图像数据和元数据打包成
CaptureResult,回调给 Framework。 - Framework 把结果传给 App,App 显示或保存。
你看,这一圈下来,每一层都在做自己分内的事。任何一个环节慢了,都会影响用户体验。
我的建议: 调试Camera性能问题时,先别急着看代码。先搞清楚数据流卡在哪一层。用 perf 或者 ftrace 抓一下内核事件,看看 DQBUF 的等待时间,基本就能定位问题。