第三章:Gralloc与GraphicBuffer——内存分配的幕后推手

各位好,欢迎来到第三章。这一章我们聊聊Gralloc和GraphicBuffer。说实话,这两个东西是Camera HAL里最绕不开的模块。你想想看,每一帧图像数据从Sensor出来,最终都要落到一块内存里,这块内存怎么来、怎么用、怎么还,全得靠它们。

我在项目中遇到过不少次,明明算法流程没问题,但就是卡顿、掉帧,最后查来查去,问题出在内存分配上。嗯,这一章我们就把它彻底讲透。

3.1 Gralloc HAL接口详解

Gralloc,全称是Graphics Memory Allocator。说白了,它就是Android系统里专门管图形缓冲区分配的那个硬件抽象层。Camera HAL要拿内存,得通过它。

Gralloc HAL的接口定义在hardware/libhardware/include/hardware/gralloc.h里。核心结构体是gralloc_module_talloc_device_t。我挑几个关键函数说说:

  • alloc():分配一块图形缓冲区。你得告诉它宽、高、像素格式、使用标志。
  • free():释放缓冲区。别漏了,漏了就是内存泄漏。
  • lock() / unlock():锁定缓冲区,拿到CPU可访问的地址。解锁后才能给硬件用。
  • registerBuffer() / unregisterBuffer():跨进程传递时,需要注册/注销缓冲区。

重点:alloc()里的usage标志位非常关键。比如GRALLOC_USAGE_HW_CAMERA_WRITE表示这块内存是给Camera硬件写的,GRALLOC_USAGE_SW_READ_OFTEN表示CPU会频繁读。选错了,性能直接打折扣。

我曾经在一个项目里,因为忘了加GRALLOC_USAGE_HW_CAMERA_WRITE,结果Sensor写数据时走的是CPU路径,帧率直接掉了一半。嗯,这种坑踩过一次就记住了。

3.2 GraphicBuffer的分配与映射

GraphicBuffer是Android上层对Gralloc的封装。你在Camera HAL里看到的sp<GraphicBuffer>,其实就是它。

分配一个GraphicBuffer很简单:

sp<GraphicBuffer> buffer = new GraphicBuffer(
    width, height, 
    HAL_PIXEL_FORMAT_YCbCr_420_888,
    GRALLOC_USAGE_HW_CAMERA_WRITE | GRALLOC_USAGE_SW_READ_OFTEN
);

但这里有个细节:GraphicBuffer的构造函数并不会立即分配物理内存。它只是创建了一个轻量级的对象,真正的内存分配发生在第一次调用lock()的时候。这叫延迟分配(Lazy Allocation),好处是节省资源,坏处是如果你忘了lock,内存根本没到位。

映射呢?说白了就是把物理内存映射到进程的虚拟地址空间。Camera HAL里常用的做法是:

void *cpuAddr = nullptr;
buffer->lock(
    GRALLOC_USAGE_SW_READ_OFTEN,
    &cpuAddr
);
// 现在cpuAddr就是你可以直接读写的内存地址
// 处理完数据后
buffer->unlock();

我的习惯:在分配GraphicBuffer时,我会先预估一下需要的buffer池大小。比如预览场景,我一般分配3-4个buffer做循环。太少容易丢帧,太多浪费内存。这个数字我建议你根据实际帧率和处理延迟来调。

3.3 BufferQueue与生产者消费者模型

BufferQueue,这个名字起得很直白——就是缓冲区的队列。它实现了一个经典的生产者-消费者模型。

在Camera HAL的场景里:

  • 生产者:Camera HAL本身。它从Sensor拿到数据,填入缓冲区。
  • 消费者:上层应用或显示系统。它从缓冲区读取数据,用于预览、拍照或编码。
  • BufferQueue:中间那个协调者。它管理着一组缓冲区,谁用完了就还给队列,谁需要就从队列取。

流程大概是这样的:

  1. 消费者通过dequeueBuffer()从队列里取一个空闲缓冲区。
  2. 消费者把缓冲区交给生产者(比如通过queueBuffer()的变种)。
  3. 生产者往缓冲区里写数据,写完后调用queueBuffer()还回去。
  4. 消费者再从队列里取,如此循环。

注意:BufferQueue是有容量上限的。默认一般是64个,但Camera场景下我建议你设小一点,比如8-16个。为什么呢?因为Camera数据量大,队列太长会导致延迟飙升。我曾经在一个4K60fps的项目里,队列设了32个,结果从拍照到回显延迟了将近半秒。后来改成8个,延迟降到了100ms以内。

还有一个容易踩的坑:死锁。如果生产者和消费者都在等对方释放缓冲区,那就卡死了。我记得有一次调试,发现预览画面卡住不动,查了半天,原来是消费者线程在等dequeue,而生产者线程在等queue,两个互相等,谁也不让谁。解决方案是加超时机制,或者用异步回调。

3.4 实战中的内存管理要点

好了,理论讲完了,我总结几个实战中一定要记住的点:

要点 说明 我的建议
usage标志位 必须准确设置,否则性能下降 Camera写+CPU读是标配
buffer池大小 太少丢帧,太多延迟 预览3-4个,录像4-6个
lock/unlock配对 漏了unlock会导致内存泄漏 用RAII封装,自动管理
跨进程传递 要用registerBuffer/unregisterBuffer 别忘了在远端注册
死锁预防 生产者和消费者互相等待 加超时或异步回调

核心思想:内存管理不是越多越好,也不是越快越好,而是刚刚好。你要根据实际的帧率、分辨率、处理延迟来动态调整。我一般会在HAL里加一个调试接口,可以实时查看buffer池的使用情况,这样调优起来就方便多了。

嗯,这一章就到这里。Gralloc和GraphicBuffer是Camera HAL的基石,BufferQueue是协调的枢纽。搞懂了它们,你就能理解为什么有时候换一块内存分配策略,整个系统的流畅度就完全不一样了。下一章我们聊聊更具体的Buffer管理策略,到时候见。