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的架构其实不复杂。我画个图你就明白了。

ION内存分配器框架 用户空间 (User Space) 应用 / SurfaceFlinger / Camera HAL / Codec ION Core (内核空间) ioctl接口 / 缓冲区分配与释放 / DMA-BUF导出 ION Heap system-heap (非连续内存) ION Heap carveout-heap (预留连续内存) ION Heap cma-heap (CMA连续内存) ION Heap 自定义 (厂商扩展) 硬件层 (Hardware) GPU / Display Controller / ISP / Video Codec

框架分三层:

  • 用户空间: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最典型的应用场景。我简单说一下流程:

  1. SurfaceFlinger 通过ION分配一块图形缓冲区。
  2. 这块缓冲区被包装成 GraphicBuffer 对象,内部持有DMA-BUF的fd。
  3. 应用通过 BufferQueue 机制,把GraphicBuffer在应用和SurfaceFlinger之间传递。
  4. GPU渲染完成后,缓冲区被送到 HWC(硬件合成器),HWC通过DMA-BUF直接读取像素数据。
  5. 显示控制器从帧缓冲区读取数据,刷新屏幕。

整个过程,数据始终在同一块物理内存里流转。没有拷贝,没有搬移。这就是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