15、显示缓冲区管理:ION/DMA-BUF框架、GraphicBuffer分配、Fence同步机制、BufferQueue
2026/9/7 4:53:17 网站建设 项目流程

15.1 ION/DMA-BUF框架:内存分配的基石

先讲ION。这是Android系统里统一的内存分配器,专门给多媒体、显示、相机这些模块用的。

为什么需要ION?因为显示驱动需要的内存不是普通的内存。它需要连续物理内存(或者至少是IOMMU映射好的内存),还要支持跨进程共享。你想想看,应用层画了一帧画面,要传给SurfaceFlinger合成,再送到显示硬件——这中间要跨多少道进程?

ION的核心概念就是「Heap」。每个Heap代表一种内存类型:

Heap类型用途特点
ION_HEAP_TYPE_SYSTEM普通系统内存非连续,速度一般
ION_HEAP_TYPE_SYSTEM_CONTIG连续物理内存用于显示控制器直接访问
ION_HEAP_TYPE_CARVEOUT预留内存固定大小,不参与页回收
ION_HEAP_TYPE_CHUNK分块内存MTK平台常用,兼顾连续性和灵活性

我在MTK8678上调试时,遇到过一个问题:用SYSTEM_HEAP分配显示缓冲区,结果画面偶尔出现条纹。查了半天,发现是内存不连续导致DMA传输出错。后来换成SYSTEM_CONTIG_HEAP,问题就解决了。

关键点:显示缓冲区必须使用物理连续内存,或者通过IOMMU映射成连续的虚拟地址空间。

DMA-BUF是ION的底层机制。它本质上是一个文件描述符,代表一块可以跨设备、跨进程共享的内存。ION分配的内存,最终都会导出为一个DMA-BUF fd。

// ION分配内存示例 struct ion_allocation_data alloc_data = { .len = buffer_size, .heap_id_mask = 1 << ION_HEAP_TYPE_SYSTEM_CONTIG, .flags = ION_FLAG_CACHED, }; int ret = ioctl(ion_fd, ION_IOC_ALLOC, &alloc_data); // 获取DMA-BUF fd int dma_buf_fd = alloc_data.fd;

嗯,这里要注意:ION的API在较新内核中已经被DMA-BUF Heaps替代了。但MTK平台为了兼容性,内部还是保留了ION的接口封装。你看到代码里还有ION相关的调用,别奇怪。

15.2 GraphicBuffer分配:从应用到驱动的桥梁

GraphicBuffer是Android上层对ION/DMA-BUF的封装。应用层画图时,不会直接调ION接口,而是通过GraphicBufferAllocator来申请。

它的分配流程是这样的:

  1. 应用调用ANativeWindow::dequeueBuffer()
  2. SurfaceFlinger通过BufferQueue向GraphicBufferAllocator请求buffer
  3. GraphicBufferAllocator调用ION分配内存
  4. ION返回DMA-BUF fd,封装成GraphicBuffer对象
  5. GraphicBuffer通过Binder跨进程传递给应用

我个人习惯在调试时,先确认GraphicBuffer的格式和尺寸是否正确。MTK8678支持多种像素格式,比如RGBA_8888、RGBX_8888、NV12等。如果格式不匹配,显示硬件会直接罢工。

调试技巧:在dumpsys SurfaceFlinger的输出中,可以看到每个Layer的buffer信息。重点关注buffer的width、height、format和usage flags。

GraphicBuffer还有一个重要属性叫usage flags。它告诉ION这块内存要用来做什么:

  • GRALLOC_USAGE_HW_RENDER:给GPU渲染用
  • GRALLOC_USAGE_HW_COMPOSER:给显示合成器用
  • GRALLOC_USAGE_HW_VIDEO_ENCODER:给视频编码器用
  • GRALLOC_USAGE_SW_READ_OFTEN:CPU频繁读取

这些flags会影响内存的cache策略和物理位置。比如HW_COMPOSER通常要求uncached内存,因为显示控制器不经过CPU cache。

15.3 Fence同步机制:别让画面撕裂

多屏显示最头疼的问题是什么?同步。

你想想看,GPU在渲染第N帧,显示控制器在读取第N-1帧,如果两者同时操作同一块buffer,画面就会撕裂。

Fence就是用来解决这个问题的。它本质上是一个内核态的同步原语,用文件描述符表示。

Fence有两种状态:

  • signaled:操作已完成,可以安全使用buffer
  • unsignaled:操作还在进行中,别碰buffer

典型的同步流程是这样的:

  1. GPU开始渲染buffer A,创建一个release fence
  2. GPU渲染完成后,signal这个fence
  3. 显示控制器等待这个fence signaled后,才开始读取buffer A
  4. 显示控制器读取完成后,创建另一个release fence
  5. GPU等待这个fence,才能把新数据写入buffer A

我曾经在MTK8678上遇到一个诡异的问题:双屏显示时,副屏偶尔卡住不动。查了三天,发现是Fence超时导致的。主屏的release fence一直没signal,副屏的acquire fence等不到,就卡死了。

避坑指南:我曾经因为Fence超时时间设置太短,导致正常操作也被误判为超时。建议把超时时间设为5秒以上,调试阶段甚至可以设到10秒。

在代码层面,Fence的操作主要通过sync_file接口:

// 创建fence int fence_fd = sync_file_create("gpu_render_fence"); // 等待fence struct sync_fence_info_data *info; int ret = ioctl(fence_fd, SYNC_IOC_FENCE_INFO, info); // 合并多个fence int merged_fd = sync_file_merge("merged_fence", fence1_fd, fence2_fd);

15.4 BufferQueue:生产者-消费者模型

BufferQueue是Android显示系统的核心数据结构。它实现了生产者-消费者模型,让应用(生产者)和SurfaceFlinger(消费者)能高效地交换buffer。

BufferQueue的核心成员:

成员作用
mSlots[]存储所有buffer的数组,默认64个slot
mQueue已填充好的buffer队列,等待消费者处理
mFreeSlots空闲slot列表
mActiveBuffers当前正在被生产或消费的buffer

它的工作流程,说白了就是三个操作:

  • dequeueBuffer:生产者申请一个空闲buffer
  • queueBuffer:生产者把填充好的buffer放入队列
  • acquireBuffer:消费者从队列取出buffer
  • releaseBuffer:消费者用完buffer后归还

多屏场景下,每个显示设备都有自己的BufferQueue。MTK8678支持三个屏幕同时显示,那就需要三个独立的BufferQueue实例。

这里有个坑:BufferQueue的深度(即mQueue中最多能放几个buffer)会影响延迟。深度太小,生产者容易阻塞;深度太大,画面延迟会增加。我一般建议设成2或3,兼顾流畅度和延迟。

核心要点:BufferQueue + Fence + ION这三者配合,构成了Android显示系统的完整数据通路。ION负责「在哪里放数据」,Fence负责「什么时候可以动数据」,BufferQueue负责「数据怎么流转」。

15.5 实战经验:多屏显示的内存优化

最后分享一个我在MTK8678项目中的优化经验。

当时客户要求三屏同时显示1080p视频,每个屏幕60fps。算一下:1920×1080×4字节×3屏×60fps ≈ 1.5GB/s的带宽。这还没算GPU和CPU的访问。

内存带宽很快就成了瓶颈。我做了几件事:

  1. 使用AFBC压缩:ARM的帧缓冲压缩技术,可以减少30%-50%的带宽
  2. 调整ION heap分配策略:把显示buffer放在专用的carveout内存里,避免和系统内存争抢
  3. 优化Fence等待逻辑:减少不必要的同步等待,让流水线更紧凑
  4. BufferQueue深度调优:从默认的1改成2,让GPU和显示控制器能并行工作

优化后,带宽占用降到了800MB/s左右,三屏播放流畅无卡顿。

嗯,这一章的内容就到这里。缓冲区管理是显示驱动的核心,也是问题最多的地方。建议你在实际调试时,先从ION分配开始排查,再看Fence同步,最后检查BufferQueue的状态。按这个顺序来,能省不少时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询