7、图形内存管理:Gralloc分配器、ION/DMA-BUF内存池、显存泄漏检测与优化

图形内存管理,说白了就是给GPU喂饭。喂慢了,掉帧;喂错了,花屏;喂多了,内存泄漏。我做了这么多年图形优化,见过太多应用因为显存泄漏被系统直接杀掉——那场面,真叫一个惨烈。

这一章,咱们就聊聊Android图形内存的底层机制。从Gralloc分配器到ION/DMA-BUF内存池,再到显存泄漏的检测与优化,我会把我在项目中踩过的坑、总结的经验,全都抖出来。

7.1 Gralloc分配器:图形内存的管家

Gralloc是什么?它是Android图形系统的内存分配器。每次你要显示一帧画面,系统都得找Gralloc要一块内存。这块内存不是普通的堆内存,而是专门给GPU用的显存。

我个人习惯把Gralloc理解成一个「内存中介」。它从ION内存池里拿大块内存,然后按需切分给SurfaceFlinger、应用进程这些「客户」。你想想看,如果没有Gralloc,每个应用都直接去操作ION,那不乱套了?

7.1.1 Gralloc的工作流程

当一个应用请求一块图形缓冲区时,流程大概是这样的:

  1. 应用通过SurfaceFlinger发起buffer申请
  2. SurfaceFlinger调用Gralloc的alloc接口
  3. Gralloc从ION/DMA-BUF中分配物理连续内存
  4. 返回一个buffer handle给应用
  5. 应用渲染完成后,通过Gralloc归还buffer

这里有个关键点:buffer handle只是一个文件描述符,真正的物理内存还在ION里管着。这种设计让跨进程共享图形内存变得非常高效——你不需要拷贝数据,只需要传递一个fd。

核心概念:Gralloc分配的内存是物理连续的,这对GPU和显示控制器来说至关重要。非连续内存会导致DMA传输失败,画面直接崩掉。

7.1.2 Gralloc的HAL层实现

Gralloc是一个HAL模块,不同厂商有自己的实现。高通、联发科、三星,每家都有自己的Gralloc实现。但接口是统一的:

// Gralloc HAL接口(简化版)
typedef struct gralloc_module_t {
    int (*alloc)(struct alloc_device_t* dev,
                 int w, int h, int format, int usage,
                 buffer_handle_t* handle, int* stride);
    int (*free)(struct alloc_device_t* dev,
                buffer_handle_t handle);
    int (*lock)(struct alloc_device_t* dev,
                buffer_handle_t handle, int usage,
                int l, int t, int w, int h,
                void** vaddr);
    int (*unlock)(struct alloc_device_t* dev,
                  buffer_handle_t handle);
} gralloc_module_t;

你看,接口很简单。但实现起来,各家差异很大。我记得有一次调试高通平台的设备,发现alloc返回的stride值跟预期不一样,导致纹理映射错位。查了两天才发现是高通Gralloc对某些格式做了特殊对齐处理。

避坑指南:千万不要假设stride等于width。不同平台、不同格式的stride可能不同。我曾经因为这个假设,在某个平板上遇到了花屏问题,排查了整整一天。

7.2 ION/DMA-BUF内存池:底层的内存基石

Gralloc是前台管家,ION才是真正的「金库」。ION是Android引入的统一内存分配器,它管理着系统的物理内存池,专门给多媒体、图形、相机这些硬件模块用。

DMA-BUF是Linux内核的机制,ION基于它实现。说白了,DMA-BUF就是一块可以被多个硬件设备共享的物理内存。GPU、显示控制器、视频编解码器,都可以访问同一块DMA-BUF。

7.2.1 ION的Heap类型

ION把物理内存分成不同的heap,每种heap有不同的特性:

Heap类型 说明 典型用途
ION_HEAP_TYPE_SYSTEM 非连续内存,通过vmalloc分配 CPU访问的缓冲区
ION_HEAP_TYPE_SYSTEM_CONTIG 物理连续内存,通过kmalloc分配 小尺寸连续内存需求
ION_HEAP_TYPE_CARVEOUT 预留的固定物理内存区域 显示控制器、GPU专用
ION_HEAP_TYPE_CHUNK 分块连续内存 视频编解码器
ION_HEAP_TYPE_DMA 通过DMA API分配的连续内存 Camera、GPU

嗯,这里要注意:CARVEOUT类型的heap是固定的,系统启动时就划好了。如果Gralloc从CARVEOUT里分配内存,用完了不及时归还,那其他模块就申请不到内存了。我遇到过一款设备,相机和GPU共用CARVEOUT,结果相机启动时GPU占着内存不放,直接导致相机黑屏。

7.2.2 ION的分配与共享机制

ION分配内存的核心是ioctl调用。应用层通过文件描述符来操作:

// ION分配示例
int ion_alloc(int ion_fd, size_t len, size_t align,
              unsigned int heap_mask, unsigned int flags,
              int *fd) {
    struct ion_allocation_data data = {
        .len = len,
        .align = align,
        .heap_id_mask = heap_mask,
        .flags = flags,
    };
    
    int ret = ioctl(ion_fd, ION_IOC_ALLOC, &data);
    if (ret < 0) return ret;
    
    *fd = data.fd;
    return 0;
}

// 共享给其他进程
// 只需要传递fd,通过unix socket或binder
// 接收方用fd映射到自己的地址空间
void* map_ion_buffer(int fd, size_t len) {
    return mmap(NULL, len, PROT_READ | PROT_WRITE,
                MAP_SHARED, fd, 0);
}

你看,整个过程就是:分配→拿到fd→mmap→使用→munmap→关闭fd。简单吧?但问题就出在「关闭fd」这一步。很多人忘了close,或者mmap后忘了munmap,内存就泄漏了。

警告:ION内存泄漏的典型症状是:系统运行一段时间后,相机打不开、游戏闪退、界面卡顿。用dumpsys meminfo查看,会发现graphics内存持续增长,永不回落。

7.3 显存泄漏检测与优化

显存泄漏,说白了就是「借了不还」。应用申请了图形缓冲区,用完了没归还给Gralloc,或者ION的fd没关闭,内存就一直在那里占着。

我做过一个项目,某个视频应用播放4小时后,显存占用从50MB涨到了800MB,最后系统直接OOM杀进程。查下来,是MediaCodec的Surface模式没有正确释放buffer。

7.3.1 检测工具与方法

检测显存泄漏,我常用的工具和方法有这些:

  • dumpsys meminfo:最直接的工具。看Graphics和GL mtrack两项,如果持续增长,就有问题。
  • dumpsys gfxinfo:查看帧缓冲区的分配情况。
  • dumpsys SurfaceFlinger:查看所有Surface的buffer状态。
  • /d/ion/:ION的debugfs节点,可以查看每个heap的使用情况。
  • SimplePerf + 火焰图:定位频繁分配内存的调用栈。

我个人习惯先用dumpsys meminfo做快速筛查。如果发现graphics内存异常,再用dumpsys SurfaceFlinger看具体是哪个Surface在泄漏。

# 快速检查显存使用
adb shell dumpsys meminfo | grep -E "Graphics|GL"

# 查看ION heap使用情况
adb shell cat /d/ion/heaps

# 查看SurfaceFlinger的buffer状态
adb shell dumpsys SurfaceFlinger --list
adb shell dumpsys SurfaceFlinger [layer_name]

7.3.2 常见泄漏场景与修复

我总结了几种最常见的显存泄漏场景:

场景 原因 修复方案
SurfaceView未释放 Activity销毁时没有释放Surface 在onPause/onDestroy中调用surface.release()
TextureView泄漏 SurfaceTexture引用未清理 确保SurfaceTexture.release()被调用
MediaCodec Surface模式 解码器停止后buffer未归还 调用codec.stop()后,再调用surface.release()
自定义Gralloc分配 alloc后没有对应的free 使用try-with-resources或finally块确保释放
ION fd泄漏 mmap后没有munmap,或fd没有close 使用RAII模式管理fd生命周期

我曾经遇到过一个特别隐蔽的泄漏:某个应用使用OpenGL的FBO(帧缓冲对象),每次渲染都创建一个新的FBO,但忘记删除旧的。FBO本身不占多少内存,但它关联的纹理和渲染缓冲区占的是显存。结果就是,应用跑了一晚上,显存占用从100MB涨到了2GB。

避坑指南:使用OpenGL时,一定要成对调用glGenFramebuffers和glDeleteFramebuffers。我习惯在创建FBO时,就把删除逻辑写在finally块里,或者用弱引用管理。

7.3.3 优化策略

优化显存使用,我总结了几个实用策略:

  • 复用缓冲区:不要频繁创建和销毁缓冲区。用BufferPool模式,提前分配好一批缓冲区,循环使用。
  • 按需分配:不要提前分配大量显存。比如视频播放,根据实际分辨率分配,不要直接分配4K的缓冲区。
  • 及时回收:用WeakReference或PhantomReference监控缓冲区生命周期,确保GC时能触发回收。
  • 压缩纹理:使用ETC2、ASTC等压缩格式,可以减少显存占用。我见过一个游戏,纹理从RGBA8888改成ETC2后,显存占用直接降了60%。
  • 降低分辨率:如果不需要全分辨率渲染,可以降低缓冲区尺寸。比如UI界面,用1080p就够了,没必要用4K。

嗯,这里要特别说一下BufferPool。我做过一个视频墙应用,同时显示16路视频。如果不复用缓冲区,每路视频都要分配独立的解码和显示缓冲区,显存直接爆掉。用了BufferPool后,只分配了4组缓冲区,16路视频轮流使用,显存占用降低了75%。

7.4 图形内存架构总览

为了让你更直观地理解整个图形内存管理体系,我画了一张架构图:

Android图形内存架构总览 应用层 (App Process) SurfaceView / TextureView / Canvas / OpenGL ES SurfaceFlinger Buffer Queue管理 / Layer合成 / HWC交互 Gralloc HAL alloc / free / lock / unlock ION / DMA-BUF 内存池 SYSTEM Heap CARVEOUT Heap DMA Heap CHUNK Heap GPU / Display Controller / Camera / Video Codec 厂商自定义实现 物理内存管理

这张图展示了从应用层到硬件层的完整内存流转路径。你看,每一层都有明确的职责:应用层只管用,SurfaceFlinger管调度,Gralloc管分配,ION管物理内存。这种分层设计让各层可以独立优化,也方便厂商做定制。

但分层也带来了问题:泄漏可能发生在任何一层。应用层忘了释放Surface,Gralloc层忘了归还buffer,ION层忘了关闭fd——任何一个环节出问题,都会导致显存泄漏。

核心观点:显存泄漏的根因,99%是「引用计数管理不当」。无论是Gralloc的buffer handle,还是ION的fd,本质上都是引用计数对象。谁拿走了引用,谁就要负责归还。这个道理说起来简单,但实践中经常被忽略。

好了,这一章的内容就到这里。图形内存管理是个系统工程,从Gralloc到ION,从分配到回收,每个环节都需要细心打理。下一章,我们会聊聊更具体的性能优化技巧——如何用工具定位掉帧问题。


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