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_START和DMA_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怎么传?两种方式:
- Binder:传文件描述符(fd)。Binder本身支持fd传递,内核会自动处理。
- 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池用完了,没有及时回收。
怎么解决?我的经验是:
- 加超时机制。等Buffer超过100ms就报错,别死等。
- 用双Buffer或三Buffer。一个在DMA,一个在显示,一个在等待。
- 做好引用计数。每个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。
优化步骤:
- 用CMA heap,别用carveout。CMA可以动态调整。
- Buffer大小按64字节对齐,避免cache miss。
- 用dmabuf做跨进程传递,减少拷贝。
- 加一个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流转过程:
嗯,这张图把整个流程说清楚了。ISP从Buffer池取空Buffer写入,CPU处理,Display显示,最后归还到池里。死锁就发生在互相等待的时候,解决方案就是超时+多Buffer+引用计数。
我的经验:调试Buffer问题时,多用dumpsys和cat /d/ion/*查看ION使用情况。能看到每个Buffer被谁占着,是不是泄漏了。
好了,这一章的内容就到这里。Buffer管理是个细活,多调多试,慢慢就有感觉了。