3、Buffer分配器:Gralloc接口详解、ION/DMA-BUF分配器、Buffer属性协商

好,咱们继续往下聊。上一节我们把Buffer的生命周期和流转机制讲清楚了,这一节我打算深入到底层——Buffer到底是怎么分配出来的?谁在背后干活?

说实话,我早年刚接触Camera HAL时,对Gralloc的理解就是“一个分配内存的东西”。后来踩了不少坑才明白,Gralloc远不止分配内存那么简单。它其实是整个Android图形系统的内存管理中枢,Buffer的属性协商、内存类型选择、跨进程共享,全得靠它。

3.1 Gralloc接口:HAL与图形系统的桥梁

Gralloc的全称是Graphics Memory Allocator,它定义在Android的硬件抽象层(HAL)中。说白了,它就是一套标准接口,让上层Framework和底层驱动能对上话。

我个人习惯把Gralloc理解成“内存中介”。上层说“我要一块宽1920、高1080、格式NV12的Buffer”,Gralloc就去底层找ION或DMA-BUF要一块符合要求的内存,然后返回一个句柄给上层。

Gralloc的核心接口主要有这几个:

  • alloc():分配Buffer,传入宽、高、格式、用法标志等参数
  • free():释放Buffer
  • lock() / unlock():锁定Buffer获取CPU访问地址,解锁后归还给GPU或硬件
  • getBufferDescriptor():获取Buffer的描述信息

这里有个关键点——用法标志(usage flags)。你分配Buffer时得告诉Gralloc这块内存打算怎么用:是给Camera用?给Display用?还是给Video编码器用?不同的用法会决定内存类型和分配策略。

常见用法标志举例:

  • GRALLOC_USAGE_SW_READ_OFTEN:CPU频繁读取,比如做图像处理
  • GRALLOC_USAGE_HW_CAMERA_WRITE:Camera硬件写入,比如ISP输出
  • GRALLOC_USAGE_HW_TEXTURE:GPU纹理,比如用于渲染
  • GRALLOC_USAGE_HW_VIDEO_ENCODER:视频编码器使用

我在项目中遇到过一个问题:某个平台上的Camera预览画面总是卡顿,查了半天发现是Buffer的用法标志没配对。Camera输出的Buffer被标记成了SW_READ_OFTEN,结果系统给分配了CPU可访问的内存,导致GPU访问时效率极低。改成HW_CAMERA_WRITE | HW_TEXTURE后,问题就解决了。

3.2 ION分配器:曾经的王者

ION是Google在Android 4.3引入的内存分配器,它取代了之前的PMEM(Physical Memory)。ION的设计思路很简单——提供一个统一的接口,底层可以对接不同的内存类型。

ION支持的内存类型(heap)包括:

Heap类型 说明 典型用途
ION_HEAP_TYPE_SYSTEM 系统堆,分配普通物理内存 CPU访问的Buffer
ION_HEAP_TYPE_SYSTEM_CONTIG 连续物理内存的系统堆 某些DMA设备需要连续内存
ION_HEAP_TYPE_CARVEOUT 预留内存区域 显示控制器、Camera ISP等
ION_HEAP_TYPE_CHUNK 分块内存 某些特殊硬件

ION的分配流程大致是这样的:

// 伪代码示意
int ion_fd = open("/dev/ion", O_RDONLY);
struct ion_allocation_data alloc_data;
alloc_data.len = buffer_size;
alloc_data.heap_id_mask = ION_HEAP_TYPE_SYSTEM_MASK;
alloc_data.flags = ION_FLAG_CACHED;

ioctl(ion_fd, ION_IOC_ALLOC, &alloc_data);
// alloc_data.fd 就是分配到的dma-buf文件描述符

嗯,这里要注意一点——ION分配出来的内存是通过dma-buf机制共享的。你拿到的是一个文件描述符(fd),而不是直接的内存地址。这个fd可以跨进程传递,底层内核会自动处理页表映射。

我的经验:ION在Android 10之前是主流,但从Android 11开始,Google逐步废弃了ION,转向了DMA-BUF Heaps。如果你在维护老平台,ION的知识还是得懂。我去年还帮一个客户排查过ION内存泄漏的问题,最后发现是某个驱动没有正确释放dma-buf引用。

3.3 DMA-BUF Heaps:新时代的分配器

DMA-BUF Heaps是Linux内核主线中的机制,Google在Android 11中正式采用。它比ION更简洁、更标准化。

为什么Google要换?说白了,ION是Android特有的,上游Linux内核不愿意合入。而DMA-BUF Heaps是内核社区认可的方案,维护成本更低,兼容性更好。

DMA-BUF Heaps的核心概念和ION类似,但接口更干净:

  • /dev/dma_heap/system:非连续内存堆
  • /dev/dma_heap/system-uncached:非缓存版本
  • /dev/dma_heap/cma:连续内存分配器(Contiguous Memory Allocator)

分配方式也差不多:

// DMA-BUF Heaps分配示例
int dma_heap_fd = open("/dev/dma_heap/system", O_RDONLY);
struct dma_heap_allocation_data alloc_data;
alloc_data.len = buffer_size;
alloc_data.fd_flags = O_RDWR | O_CLOEXEC;
alloc_data.heap_flags = 0;

ioctl(dma_heap_fd, DMA_HEAP_IOC_ALLOC, &alloc_data);
// alloc_data.fd 就是dma-buf文件描述符

你想想看,这跟ION的代码是不是很像?其实底层机制是一样的,只是接口换了个马甲。但DMA-BUF Heaps有一个好处——它直接暴露给用户空间,不需要额外的HAL层封装。

注意:DMA-BUF Heaps分配的内存默认是uncached的。如果你需要CPU频繁读写,记得加上DMA_HEAP_IOC_ALLOC的缓存标志,或者手动调用dma_buf_sync做缓存同步。我曾经见过一个案例,开发者在DMA-BUF上做CPU图像处理,忘了做缓存同步,结果图像数据全是乱的。

3.4 Buffer属性协商:一场多方博弈

Buffer分配不是Camera自己说了算的。你想想看,一块Buffer可能要被Camera写入、被GPU读取、被Display显示、被Video编码器处理。每个模块对Buffer的属性要求都不一样。

这就引出了Buffer属性协商的问题。协商的内容包括:

  • 宽高和格式:最基本的,大家都得同意
  • 对齐要求:有些硬件要求行对齐到64字节或128字节
  • 压缩格式:比如UBWC(Qualcomm)、AFBC(ARM)等硬件压缩
  • 内存类型:连续还是非连续?缓存还是不缓存?
  • 分配器选择:用ION还是DMA-BUF?用哪个heap?

在Android的Camera HAL中,这个协商过程通常发生在configureStreams()阶段。HAL会收到一个Stream列表,每个Stream都带有Consumer(消费者)的用法标志。HAL需要根据这些标志,选择一个所有参与者都能接受的Buffer属性组合。

举个例子:

// Camera HAL中协商Buffer属性的伪代码
status_t configureStreams(...) {
    for (auto& stream : streams) {
        // 获取消费者的用法标志
        uint64_t consumer_usage = stream.consumer_usage;
        
        // 加上生产者的用法标志
        uint64_t total_usage = consumer_usage | GRALLOC_USAGE_HW_CAMERA_WRITE;
        
        // 根据用法标志选择合适的格式和内存类型
        if (total_usage & GRALLOC_USAGE_HW_TEXTURE) {
            // GPU需要,考虑硬件压缩格式
            stream.format = HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED;
        } else if (total_usage & GRALLOC_USAGE_HW_VIDEO_ENCODER) {
            // 编码器需要,用NV12
            stream.format = HAL_PIXEL_FORMAT_YCbCr_420_SP;
        }
        
        // 调用Gralloc分配Buffer
        buffer_handle_t buffer;
        gralloc->alloc(stream.width, stream.height, 
                       stream.format, total_usage, &buffer);
    }
}

这里有个坑——HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED。这个格式的意思是“让Gralloc自己决定最佳格式”。我刚开始用的时候觉得这太随意了,后来才明白,这是为了给硬件厂商最大的灵活性。比如Qualcomm的平台可能会选择UBWC压缩格式,而MTK的平台可能选择其他格式。

协商失败怎么办?

如果协商失败,比如Camera输出的格式Display不支持,那HAL就得做格式转换。常见的做法是分配两个Buffer池:一个给Camera用(比如NV12),另一个给Display用(比如RGB888),中间通过GPU或硬件转码器做转换。代价就是多一次内存拷贝和额外的延迟。

3.5 实战中的Buffer分配策略

讲了这么多理论,最后我分享几个实战中的策略:

  1. 优先使用IMPLEMENTATION_DEFINED:让Gralloc和硬件厂商的驱动去选择最优格式,除非你有明确的格式要求。
  2. 合理设置用法标志:不要滥用SW_READ_OFTEN,CPU访问会破坏GPU的缓存优化。我见过有人把所有Buffer都加上这个标志,结果性能惨不忍睹。
  3. 注意内存对齐:不同硬件对行对齐的要求不同。Camera ISP可能要求16字节对齐,而GPU可能要求64字节。分配时取最大值。
  4. Buffer池预分配:不要在每一帧都分配和释放Buffer,那样会产生大量内存碎片。建议在Stream配置阶段就预分配好Buffer池。
  5. 监控内存使用:通过dumpsys graphics/proc/meminfo监控Buffer内存占用。我曾经遇到过Buffer泄漏导致系统OOM的案例,最后发现是某个Stream关闭时没有正确释放Buffer池。

嗯,关于Buffer分配器,今天就聊到这儿。下一节我会讲Buffer的同步机制——Fence和ReleaseFence,这可是保证多模块协同工作的关键。到时候见。