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的数据流模型其实很直观。我画个图你就明白了:

HAL1 数据流模型 Camera Sensor 硬件层 HAL1 实现 camera_device_ops_t CameraService Framework层 原始数据 处理后数据 回调通知 数据缓冲区 (Gralloc Buffer) HAL1 直接操作硬件,将数据填入预先分配的缓冲区 Preview 预览流 Video 视频流 Still Capture 拍照流 三个流程共享同一个数据通路,通过不同的回调函数区分

你看这个图,数据从Sensor出来,经过HAL1处理,再送到Framework。但有个关键点——HAL1里这三个流程(预览、录像、拍照)是串行的。什么意思?你正在预览时想拍照,得先把预览停了,再启动拍照流程。拍完照再重新开预览。

我的经验:曾经有个项目,客户要求预览时能快速连拍。用HAL1实现时,每次拍照都要经历"停止预览→拍照→重新预览"这个循环,连拍间隔至少500ms。客户说"你们这相机也太卡了",我只好解释说这是架构限制...

Preview流程:预览是怎么跑起来的?

预览流程是HAL1里最基础也最常用的功能。调用链大概是这样的:

  1. App调用Camera.open() → Framework打开HAL设备
  2. App调用setPreviewDisplay() → 设置Surface用于显示
  3. App调用startPreview() → HAL开始从Sensor取数据
  4. HAL通过preview_stream_ops接口把数据推给SurfaceFlinger
  5. 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流程:拍照的完整链路

拍照流程和预览、录像不太一样。它走的是另一条路:

  1. App调用takePicture()
  2. HAL停止预览(如果有的话)
  3. HAL配置Sensor进入拍照模式(更高分辨率、不同曝光参数)
  4. Sensor输出一帧完整数据
  5. HAL做JPEG编码(或者YUV→JPEG转换)
  6. 通过data_callback把JPEG数据回调给Framework
  7. Framework把JPEG写入文件
  8. 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