Camera HAL1详解:从接口定义到实战避坑
各位同学,今天我们来聊聊Android相机系统里一个绕不开的话题——HAL1。说实话,我在刚接触Android相机时,HAL1已经算是"老古董"了。但你别小看它,直到今天,很多低端设备和定制ROM还在用这套架构。理解HAL1,是你吃透整个相机系统的第一步。
HAL1接口定义:它到底长什么样?
HAL1的全称是Camera Hardware Abstraction Layer version 1。说白了,它就是Google定义的一套标准接口,让上层Framework和底层硬件驱动能对上话。
核心接口定义在hardware/libhardware/include/hardware/camera.h里。我带你看看关键结构体:
typedef struct camera_device {
hw_device_t common;
camera_device_ops_t *ops;
} camera_device_t;
typedef struct camera_device_ops {
int (*set_preview_window)(struct camera_device *,
struct preview_stream_ops *);
int (*start_preview)(struct camera_device *);
void (*stop_preview)(struct camera_device *);
int (*take_picture)(struct camera_device *);
int (*start_recording)(struct camera_device *);
// ... 还有几十个函数指针
} camera_device_ops_t;
嗯,这里要注意:camera_device_ops_t里全是函数指针。每个厂商的HAL实现,就是把这些函数指针填上自己的逻辑。我当年第一次看这个结构体时,心里想的是——这不就是个C语言版的虚函数表吗?
关键点:HAL1的所有操作都是同步阻塞的。你调用start_preview(),它必须等硬件真正启动后才返回。这个设计在后面会带来大麻烦。
数据流模型:数据是怎么跑的?
HAL1的数据流模型其实很直观。我画个图你就明白了:
你看这个图,数据从Sensor出来,经过HAL1处理,再送到Framework。但有个关键点——HAL1里这三个流程(预览、录像、拍照)是串行的。什么意思?你正在预览时想拍照,得先把预览停了,再启动拍照流程。拍完照再重新开预览。
我的经验:曾经有个项目,客户要求预览时能快速连拍。用HAL1实现时,每次拍照都要经历"停止预览→拍照→重新预览"这个循环,连拍间隔至少500ms。客户说"你们这相机也太卡了",我只好解释说这是架构限制...
Preview流程:预览是怎么跑起来的?
预览流程是HAL1里最基础也最常用的功能。调用链大概是这样的:
- App调用
Camera.open()→ Framework打开HAL设备 - App调用
setPreviewDisplay()→ 设置Surface用于显示 - App调用
startPreview()→ HAL开始从Sensor取数据 - HAL通过
preview_stream_ops接口把数据推给SurfaceFlinger - SurfaceFlinger合成后显示到屏幕
这里有个坑:set_preview_window()传进来的preview_stream_ops,其实是Framework分配好的一组Gralloc Buffer。HAL要做的事情很简单——把Sensor数据填进去。但填数据的方式决定了性能。
// HAL1 预览数据推送的典型实现
static int preview_stream_ops_callback(buffer_handle_t *buffer, void *user) {
// 拿到buffer后,把sensor数据memcpy进去
// 或者更高效的方式:直接让DMA把数据写入这个buffer
memcpy(buffer->data, sensor_raw_data, buffer_size);
// 通知Framework:数据准备好了
stream->ops->enqueue_buffer(stream, buffer);
return 0;
}
注意:这里如果做memcpy,CPU占用会很高。我见过一个方案,厂商直接在ISP里把数据DMA到Gralloc Buffer的物理地址上,性能直接翻倍。但这么做需要你了解底层内存管理,搞不好容易出内存踩踏。
Video流程:录像和预览有啥区别?
说实话,HAL1里录像和预览的数据流几乎一模一样。区别在于:
- 预览:数据送到SurfaceFlinger显示
- 录像:数据同时送到MediaCodec编码
但HAL1有个限制——它只能输出一种格式的数据。你预览用的是NV21,录像也得是NV21。MediaCodec拿到NV21后自己转成编码器需要的格式。这中间多了一次格式转换,功耗和延迟都上去了。
我记得有个项目做720p录像,帧率死活上不去。查了半天,发现是NV21转YUV420SP的转换函数写得稀烂,CPU全耗在格式转换上了。换成NEON优化后的转换函数,帧率直接从22fps飙到30fps。
Still Capture流程:拍照的完整链路
拍照流程和预览、录像不太一样。它走的是另一条路:
- App调用
takePicture() - HAL停止预览(如果有的话)
- HAL配置Sensor进入拍照模式(更高分辨率、不同曝光参数)
- Sensor输出一帧完整数据
- HAL做JPEG编码(或者YUV→JPEG转换)
- 通过
data_callback把JPEG数据回调给Framework - Framework把JPEG写入文件
- App重新调用
startPreview()恢复预览
你看第2步和第8步,这就是HAL1最让人头疼的地方——拍照前后都要操作预览状态。如果拍照过程中出了异常,预览可能就恢复不了了。
避坑指南:我曾经遇到一个bug,拍照后预览黑屏。排查发现是HAL在拍照过程中把Sensor的时钟关了,但异常退出时没重新打开。解决方案是在takePicture()的异常分支里加一个强制恢复预览状态的逻辑。
HAL1的局限性:为什么Google要抛弃它?
聊完流程,我们来谈谈HAL1的硬伤。说实话,这些局限性不是Google设计得不好,而是移动端相机需求发展太快,HAL1跟不上了。
| 问题 | 具体表现 | 影响 |
|---|---|---|
| 串行操作 | 预览/录像/拍照不能同时进行 | 连拍慢、切换卡顿 |
| 同步阻塞 | 每个操作都要等硬件响应 | UI线程容易卡死 |
| 单一数据流 | 只能输出一种格式/分辨率 | 预览和录像不能独立配置 |
| 无Buffer管理 | HAL直接操作Gralloc Buffer | 容易出内存问题 |
| 扩展性差 | 新增功能需要加函数指针 | 厂商定制困难 |
你想想看,现在的手机相机功能多复杂?HDR、夜景、人像、慢动作、双摄虚化...这些在HAL1上实现起来,要么做不到,要么做出来体验极差。
举个具体的例子:双摄虚化。HAL1只支持一个Camera设备,你要做双摄就得开两个Camera实例。但两个实例之间怎么同步?曝光时间怎么对齐?这些HAL1根本没考虑。厂商只能自己加私有接口,搞得代码又臭又长。
我的看法:HAL1就像一辆手动挡的老爷车,开起来有感觉,但堵车时左脚踩离合踩到抽筋。Google后来推HAL3,本质上就是给相机系统装了个自动变速箱。但话说回来,理解手动挡的原理,你才能更好地驾驭自动挡。
好了,HAL1的内容就讲到这里。下一章我们会深入HAL3,看看Google是怎么解决这些问题的。到时候你会发现,HAL3的很多设计思路,其实都是在给HAL1的坑填土。
公众号:蓝海资料掘金营,微信deep3321