10、Buffer管理与内存优化:从ION到实战

各位同学,今天我们来啃一块硬骨头——Buffer管理。说实话,这是Camera驱动开发里最容易出问题的地方,也是最能体现功力的地方。我在做4K@60fps项目时,光是Buffer死锁就调了整整两周。今天我把这些经验全部分享给你们。

核心观点:Buffer管理不是简单的内存分配,而是整个Camera pipeline的血液系统。血液堵了,再好的算法也跑不动。

10.1 ION/DMA-BUF Heap详解

先说说ION。它是Android的内存分配器,专门为多媒体场景设计。说白了,就是帮Camera、Display、Video这些模块分配共享内存。

ION里有三种Heap,我一个个讲:

Heap类型特点适用场景
system heap从系统内存分配,走页缓存小分辨率、低帧率
carveout heap预分配物理连续内存需要物理连续的场景
CMA heap动态预留,按需分配4K@60fps这种大内存场景

我个人习惯,能选CMA就不选carveout。为什么?carveout是静态预留的,你预留多了浪费,预留少了不够用。CMA是动态的,平时给别的模块用,Camera需要时再拿过来。我在一个项目中,carveout预留了64MB,结果4K@60fps需要80MB,直接崩了。换成CMA后,问题解决。

小技巧:用CMA时,记得调大cma=参数。我一般设256MB起步,具体看你的分辨率需求。

10.2 Gralloc HAL实现

Gralloc是Android的图形内存分配器。Camera Buffer最终要通过Gralloc交给SurfaceFlinger显示。这里有几个关键函数:

  • allocate:分配Buffer。注意对齐要求,一般是4K对齐。
  • free:释放Buffer。别漏了,漏了就是内存泄漏。
  • registerBuffer:把Buffer注册到当前进程。跨进程传递时必用。

我曾经遇到一个坑:allocate时没做cache一致性处理,结果图像花屏。后来加了DMA_BUF_SYNC_STARTDMA_BUF_SYNC_END才搞定。

// Gralloc allocate示例
int gralloc_allocate(alloc_device_t* dev, int w, int h, int format, int usage, buffer_handle_t* handle, int* stride) {
    // 1. 计算大小,注意对齐
    size_t size = ALIGN(w, 64) * ALIGN(h, 64) * bytes_per_pixel;
    
    // 2. 通过ION分配
    int fd = ion_alloc(ion_fd, size, 0, ION_HEAP_CAMERA_MASK, 0);
    
    // 3. 映射到用户空间
    void* vaddr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    // 4. 返回handle
    *handle = (buffer_handle_t)new private_handle_t(fd, size, vaddr);
    *stride = ALIGN(w, 64);
    return 0;
}

10.3 Buffer跨进程传递

Camera HAL和Camera Service不在同一个进程,Buffer怎么传?两种方式:

  1. Binder:传文件描述符(fd)。Binder本身支持fd传递,内核会自动处理。
  2. Shared Memory:通过ashmem或dmabuf共享。适合大数据量。

我建议用Binder传fd的方式。为什么?因为dmabuf fd本身就是内核管理的,跨进程传递时内核会帮你做引用计数。你想想看,如果自己管理共享内存,还得考虑进程挂了怎么办,多麻烦。

注意:Binder传fd时,一次最多传几个?我记得是4个。如果你要传8个Buffer,得分两次。这个限制坑过我一次。

10.4 Camera Buffer死锁问题分析与解决

死锁是Camera开发中最头疼的问题。我总结了几种常见场景:

  • 场景一:HAL层等Buffer,Framework层也在等Buffer,互相等,死锁。
  • 场景二:DMA传输没完成,CPU想访问Buffer,冲突了。
  • 场景三:Buffer池用完了,没有及时回收。

怎么解决?我的经验是:

  1. 加超时机制。等Buffer超过100ms就报错,别死等。
  2. 用双Buffer或三Buffer。一个在DMA,一个在显示,一个在等待。
  3. 做好引用计数。每个Buffer被谁用了,用完了没,都要清楚。

我曾经在4K@60fps项目里遇到一个死锁:ISP的DMA引擎占着Buffer不放,CPU想读数据做算法,结果CPU等DMA,DMA等CPU释放下一个Buffer,死锁了。最后加了一个DMA中断回调,DMA完成后立即通知CPU,才解决。

10.5 实战:优化4K@60fps场景下的Buffer分配

好了,理论讲完了,我们来个实战。4K@60fps意味着什么?每帧16.6ms,4K分辨率约8.3MB一帧(YUV420)。你需要至少3个Buffer才能跑顺:

  • Buffer 0:ISP正在写入
  • Buffer 1:CPU正在处理
  • Buffer 2:Display正在显示

总内存需求:3 * 8.3MB ≈ 25MB。但别忘了,还有Metadata、Tuning数据等,建议预留32MB。

优化步骤:

  1. 用CMA heap,别用carveout。CMA可以动态调整。
  2. Buffer大小按64字节对齐,避免cache miss。
  3. 用dmabuf做跨进程传递,减少拷贝。
  4. 加一个Buffer池,预分配好,运行时直接取。
// Buffer池实现思路
class CameraBufferPool {
    int num_buffers = 4;  // 4个Buffer,比3个多一个余量
    int free_list[4];
    int free_count;
    
    BufferHandle* acquire() {
        if (free_count == 0) return NULL;  // 没Buffer了,等
        return pool[free_list[--free_count]];
    }
    
    void release(BufferHandle* buf) {
        free_list[free_count++] = buf->index;
    }
};

关键点:4K@60fps下,Buffer分配时间不能超过1ms。如果超过,就会丢帧。所以一定要预分配,不要在运行时malloc。

最后,我画了一张图,帮大家理解整个Buffer流转过程:

Camera Buffer流转图 ISP/DMA 写入Buffer CPU/算法 处理Buffer Display 显示Buffer Buffer池(4个Buffer循环使用) Buffer 0 | Buffer 1 | Buffer 2 | Buffer 3 取空Buffer 归还Buffer ⚠ 死锁高发区 ISP等CPU释放Buffer CPU等ISP写完Buffer → 互相等待,死锁 解决方案 1. 超时机制 2. 三Buffer 3. 引用计数

嗯,这张图把整个流程说清楚了。ISP从Buffer池取空Buffer写入,CPU处理,Display显示,最后归还到池里。死锁就发生在互相等待的时候,解决方案就是超时+多Buffer+引用计数。

我的经验:调试Buffer问题时,多用dumpsyscat /d/ion/*查看ION使用情况。能看到每个Buffer被谁占着,是不是泄漏了。

好了,这一章的内容就到这里。Buffer管理是个细活,多调多试,慢慢就有感觉了。

公众号:蓝海资料掘金营,微信deep3321