19、ION内存分配器:ION框架,DMA-BUF,图形缓冲区管理
说到Android的内存管理,有个东西绕不开——ION内存分配器。
我记得刚接触Android图形系统那会儿,被各种内存分配方式搞得头大。PMEM、ASHMEM、ION……名字一个比一个怪。后来Google统一推了ION,我才算真正理清了这块的逻辑。
说白了,ION就是Android里专门管大块连续内存分配的框架。尤其是图形缓冲区、相机预览、视频解码这些场景,对内存的要求特别苛刻——既要大,又要连续,还得能跨进程共享。
为什么需要ION?
你想想看,普通的malloc能搞定这些吗?
不行。malloc分配的内存是虚拟地址连续的,但物理地址不一定连续。而GPU、显示控制器、ISP这些硬件,它们要的是物理连续的内存。否则DMA传输会出问题。
在ION出现之前,Android用过PMEM(物理内存分配器)。但PMEM有个硬伤——它和硬件平台耦合太紧。换一个SoC,驱动就得重写。Google受不了这种碎片化,于是ION应运而生。
核心思想:ION把内存分配逻辑从驱动层抽出来,做成一个统一框架。上层应用和HAL层通过ION接口申请内存,底层驱动负责具体实现。这样上层代码就跨平台了。
ION框架结构
ION的架构其实不复杂。我画个图你就明白了。
框架分三层:
- 用户空间:SurfaceFlinger、Camera HAL、MediaCodec 这些组件通过ION客户端接口申请内存。
- ION Core:内核里的核心模块,负责管理所有ION客户端请求,并把请求路由到对应的Heap。
- ION Heaps:实际的内存池。不同Heap管理不同类型的内存。
ION Heap 类型
每种Heap对应一种内存特性。我列个表,你一看就明白。
| Heap类型 | 内存特性 | 典型用途 |
|---|---|---|
| system-heap | 非连续内存,通过vmalloc分配 | 普通缓冲区,CPU访问为主 |
| carveout-heap | 预留的物理连续内存 | 显示帧缓冲区,早期GPU使用 |
| cma-heap | 通过CMA分配的连续内存 | 相机、视频编解码 |
| chunk-heap | 分块连续内存 | 某些DMA设备 |
| 自定义Heap | 厂商扩展,如secure-heap | DRM保护内容 |
个人经验:我在项目中遇到过一个问题——相机预览卡顿。查了半天,发现是用了system-heap给ISP分配缓冲区。ISP需要物理连续内存,system-heap给的是非连续的,导致DMA传输频繁出错。换成cma-heap后,问题立刻解决。
DMA-BUF:ION的基石
ION分配出来的内存,最终都是以DMA-BUF的形式呈现的。
DMA-BUF是Linux内核的一个机制,专门用来在设备驱动之间共享缓冲区。它有几个关键特性:
- 跨设备共享:同一个缓冲区,GPU可以读,显示控制器可以写,CPU也能访问。
- 零拷贝:数据不需要在不同设备的内存之间搬来搬去,大家共享同一块物理内存。
- 引用计数:每个DMA-BUF都有文件描述符,通过fd的引用计数管理生命周期。
ION和DMA-BUF的关系,说白了就是:ION负责分配内存,DMA-BUF负责把这块内存共享出去。
你申请ION缓冲区时,ION会创建一个DMA-BUF对象,然后返回一个文件描述符给你。这个fd可以在进程间传递——通过Binder或者Socket。拿到fd的进程,可以用mmap映射到自己的地址空间,或者传给其他硬件驱动。
图形缓冲区管理
图形缓冲区是ION最典型的应用场景。我简单说一下流程:
- SurfaceFlinger 通过ION分配一块图形缓冲区。
- 这块缓冲区被包装成 GraphicBuffer 对象,内部持有DMA-BUF的fd。
- 应用通过 BufferQueue 机制,把GraphicBuffer在应用和SurfaceFlinger之间传递。
- GPU渲染完成后,缓冲区被送到 HWC(硬件合成器),HWC通过DMA-BUF直接读取像素数据。
- 显示控制器从帧缓冲区读取数据,刷新屏幕。
整个过程,数据始终在同一块物理内存里流转。没有拷贝,没有搬移。这就是Android图形系统高性能的秘诀之一。
关键点:ION + DMA-BUF 实现了真正的零拷贝图形管线。从GPU渲染到屏幕显示,数据只在一个地方——ION分配的物理内存里。
代码示例:申请ION缓冲区
虽然现在上层应用很少直接调用ION接口(都是通过GraphicBuffer封装),但理解底层原理还是有帮助的。我写个简化的示例:
// 打开ION设备
int ion_fd = open("/dev/ion", O_RDWR);
// 分配内存:从cma-heap申请1MB连续内存
struct ion_allocation_data alloc;
memset(&alloc, 0, sizeof(alloc));
alloc.len = 1024 * 1024; // 1MB
alloc.heap_id_mask = 1 << 2; // cma-heap的ID
alloc.flags = 0;
ioctl(ion_fd, ION_IOC_ALLOC, &alloc);
// alloc.fd 就是DMA-BUF的文件描述符
int dma_buf_fd = alloc.fd;
// 映射到用户空间
void *ptr = mmap(NULL, alloc.len,
PROT_READ | PROT_WRITE,
MAP_SHARED, dma_buf_fd, 0);
// 使用内存...
memset(ptr, 0xFF, alloc.len);
// 清理
munmap(ptr, alloc.len);
close(dma_buf_fd);
close(ion_fd);
注意:在新版内核中,ION接口已经逐渐被DMA-BUF Heaps取代。Google在Android 12之后推荐使用/dev/dma_heap接口。但底层逻辑是一样的——都是通过DMA-BUF共享内存。
我曾经踩过的坑
嗯,这里分享一个真实经历。
有一次做视频播放优化,发现播放4K视频时内存占用飙升。排查后发现,MediaCodec每次解码都从ION申请新缓冲区,解码完成后又不及时释放。ION的引用计数没归零,DMA-BUF一直活着。
问题出在哪?
应用层没有正确调用releaseBuffer。ION的DMA-BUF靠fd引用计数管理生命周期,fd不关闭,内存就不释放。后来我们在解码器状态机里加了一个强制回收逻辑——解码完成后立即关闭fd,内存占用立刻降下来了。
所以我的建议是:使用ION缓冲区时,一定要严格管理fd的生命周期。尤其是跨进程传递的场景,接收方用完必须close,否则就是内存泄漏。
ION vs 传统分配方式
最后做个对比,你就知道ION为什么是Android的标配了。
| 特性 | ION | 传统kmalloc | vmalloc |
|---|---|---|---|
| 物理连续 | 支持(cma/carveout) | 支持 | 不支持 |
| 跨设备共享 | 原生支持(DMA-BUF) | 需要额外机制 | 需要额外机制 |
| 跨进程共享 | 通过fd传递 | 不支持 | 不支持 |
| 内存碎片控制 | 好(CMA机制) | 差(容易碎片) | 一般 |
| 平台兼容性 | 高(统一接口) | 低(依赖平台) | 低(依赖平台) |
说白了,ION就是为Android这种多进程、多硬件协同的场景量身定做的。它解决了传统内存分配器解决不了的三个问题:物理连续性、跨设备共享、跨进程共享。
理解ION和DMA-BUF,你就抓住了Android图形内存管理的核心。后面讲SurfaceFlinger和BufferQueue的时候,你会发现这些概念一直在用。
公众号:蓝海资料掘金营,微信deep3321