3. 内存瓶颈分析:ion/dma-buf内存分配器、内存泄漏检测与HAL侧优化策略

内存问题,是HAL开发里最让人头疼的坑之一。我见过太多项目,跑着跑着系统就卡死,一查日志,全是ion分配失败。说白了,Android的多媒体和图形栈,几乎全靠ion/dma-buf这套机制在撑。你HAL层要是把内存玩崩了,上层应用再优化也没用。

这一章,我们就来拆解内存瓶颈的三个核心问题:ion/dma-buf怎么工作的?内存泄漏怎么抓?HAL侧怎么省内存?

3.1 ion/dma-buf内存分配器:核心机制与瓶颈

ion是Android特有的内存分配器,专门给多媒体、GPU、显示这些硬件用的。它和普通的malloc不一样,malloc分配的是虚拟内存,ion分配的是物理连续内存,或者至少是硬件能直接访问的内存。

dma-buf则是ion的上层封装,它让不同驱动之间可以共享内存。比如Camera驱动拍了张照片,通过dma-buf把内存句柄传给Display驱动,中间不用拷贝数据。零拷贝,听起来很美,但一旦出问题,排查起来也够呛。

核心概念:ion分配的是物理内存,dma-buf是共享内存的句柄。HAL层操作的是dma-buf fd(文件描述符),底层由ion驱动管理。

3.1.1 ion的分配流程

我习惯把ion的分配流程画成一张图,这样理解起来更直观:

ion/dma-buf 内存分配与共享流程 用户空间 (HAL/App) ion 驱动 (内核) 物理内存 (DMA) ioctl(ION_IOC_ALLOC) 分配物理页 返回 dma-buf fd HAL 模块 A (Camera) dma-buf 共享 HAL 模块 B (Display) 传递 fd mmap/ioctl ⚠️ 关键点: • ion分配的内存默认是物理连续的,适合DMA操作。 • dma-buf fd 是跨进程共享的句柄,不要直接传递指针。 • 分配后必须调用 close(fd) 释放,否则内存泄漏。

3.1.2 常见的瓶颈点

在实际项目中,ion分配失败是最常见的性能杀手。我遇到过几次,都是因为HAL层没有及时释放dma-buf,导致ion heap耗尽。

瓶颈类型 现象 常见原因
ion heap耗尽 分配返回 -ENOMEM dma-buf fd未关闭,或分配了超大buffer
分配延迟高 ion分配耗时 > 10ms 物理内存碎片化,或分配了不连续的大块内存
cache一致性开销 CPU访问dma-buf时性能骤降 未正确使用 dma_sync_single_for_cpu/device
我的习惯:每次分配ion内存后,立即记录fd到日志,并在析构函数中确保close。这样即使代码异常退出,也能通过日志排查泄漏。

3.2 内存泄漏检测:libmemunreachable实战

内存泄漏在HAL层特别隐蔽。为什么?因为HAL层很多代码跑在binder线程里,泄漏一次可能只丢几KB,但跑上几天,系统就崩了。Google提供了一个利器——libmemunreachable,专门用来检测native层的不可达内存。

3.2.1 libmemunreachable的原理

说白了,libmemunreachable就是遍历进程的堆内存,找到那些没有被任何指针引用的内存块。它和Java层的GC Root扫描有点像,但针对的是C/C++的malloc和mmap区域。

我记得第一次用这个工具,是在一个Camera HAL的泄漏排查中。系统跑了一晚上,ion内存涨了200MB,但不知道是谁分配的。用libmemunreachable一跑,直接定位到某个preview buffer没有释放。

3.2.2 使用方法

使用libmemunreachable很简单,不需要改代码。你只需要在设备上执行:

# 触发一次unreachable内存扫描
adb shell am dumpheap -n <pid> /data/local/tmp/heap.txt

# 或者直接调用libmemunreachable的API
# 在代码中引入 <memunreachable/memunreachable.h>
bool leak_found = GetUnreachableMemory(unreachable, &info);

扫描结果会输出所有不可达内存的调用栈。你看到最多的,往往是某个malloc或者ion分配后,指针被覆盖了,但内存没释放。

注意:libmemunreachable只能检测堆内存泄漏,无法检测dma-buf fd泄漏。dma-buf泄漏需要用 lsof 或 /sys/kernel/debug/dma_buf/bufinfo 来排查。

3.2.3 实战案例:一次HAL层泄漏排查

我曾经在一个项目里,发现系统运行12小时后,ion内存从50MB涨到了800MB。用libmemunreachable扫描,发现大量泄漏集中在某个HAL Service的堆上。

调用栈指向了一个函数:allocateGraphicBuffer()。我仔细看代码,发现每次分配buffer后,如果后续的硬件配置失败,代码直接return了,没有调用 freeBuffer()。这就是典型的「错误路径未释放」。

修复方案很简单:

// 修复前
status_t allocateAndConfigure() {
    buffer = allocateGraphicBuffer();  // 分配ion内存
    status = configureHardware();      // 可能失败
    if (status != OK) {
        return status;  // ❌ 忘记释放buffer
    }
    return OK;
}

// 修复后
status_t allocateAndConfigure() {
    buffer = allocateGraphicBuffer();
    status = configureHardware();
    if (status != OK) {
        freeGraphicBuffer(buffer);  // ✅ 确保释放
        return status;
    }
    return OK;
}
经验之谈:HAL层代码的错误处理路径,是内存泄漏的重灾区。我建议你在每个分配点后面,都写一个goto式的清理标签,或者用RAII封装dma-buf fd。

3.3 HAL侧内存优化策略

优化内存,不是让你少分配,而是让你合理分配、及时释放、减少拷贝。下面是我在多个项目中验证过的策略。

3.3.1 策略一:使用BufferQueue复用内存

不要每次请求都分配新内存。Android的BufferQueue就是干这个的。HAL层应该从BufferQueue里dequeue一个buffer,用完再queue回去。这样内存是复用的,不会频繁分配释放。

// 错误做法:每次分配新buffer
sp<GraphicBuffer> buffer = new GraphicBuffer(w, h, format, usage);
// ... 使用buffer ...
// buffer析构时释放

// 正确做法:从BufferQueue获取
sp<GraphicBuffer> buffer;
status_t err = mBufferQueue->dequeueBuffer(&buffer, &fence);
// ... 使用buffer ...
err = mBufferQueue->queueBuffer(buffer, fence);

3.3.2 策略二:按需分配,避免预分配过大内存

很多HAL实现喜欢「预分配最大尺寸」,比如Camera HAL直接分配一个4096x3072的buffer,但实际只用了1920x1080。这浪费了3倍内存。

我建议你根据实际分辨率动态分配。如果硬件支持,还可以用tiled格式或压缩格式来减少内存占用。

3.3.3 策略三:减少dma-buf的拷贝次数

dma-buf的优势就是零拷贝。但很多HAL实现为了图省事,把dma-buf里的数据读到CPU内存,处理完再写回去。这一读一写,性能就全毁了。

正确的做法是:如果硬件支持,直接在dma-buf上做处理(比如GPU渲染、DSP处理)。如果必须CPU介入,用 mmap 映射dma-buf,并注意cache同步。

// 映射dma-buf到CPU地址空间
void* cpu_addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, dma_buf_fd, 0);

// 在CPU访问前,同步cache
dma_sync_single_for_cpu(dev, phys_addr, size, DMA_FROM_DEVICE);

// ... 处理数据 ...

// 在硬件访问前,同步cache
dma_sync_single_for_device(dev, phys_addr, size, DMA_TO_DEVICE);

// 使用完后解除映射
munmap(cpu_addr, size);
避坑指南:我曾经因为忘记调用 dma_sync_single_for_cpu,导致CPU读到的数据是旧的,画面出现撕裂。这个坑排查了我整整两天。记住:CPU和硬件之间,必须显式同步cache。

3.3.4 策略四:监控ion使用情况

优化不是一次性的。我习惯在HAL层加一个监控线程,定期读取 /sys/kernel/debug/ion/heaps/ 下的统计信息。一旦发现某个heap的使用量超过阈值,就主动触发一次内存整理,或者打印警告日志。

// 读取ion heap使用量
void monitorIonUsage() {
    std::ifstream file("/sys/kernel/debug/ion/heaps/system-uncached");
    std::string line;
    while (std::getline(file, line)) {
        // 解析 "size: 12345678" 这样的行
        if (line.find("size:") != std::string::npos) {
            long size = parseSize(line);
            if (size > WARN_THRESHOLD) {
                ALOGW("ion heap usage high: %ld bytes", size);
            }
        }
    }
}

3.4 本章小结

内存瓶颈分析,说白了就是三件事:理解ion/dma-buf的分配机制、用libmemunreachable抓泄漏、在HAL侧做合理的优化策略。我个人觉得,最关键的还是养成好习惯——每次分配都想着释放,每次共享都想着同步。这样,你的HAL层才能跑得稳、跑得久。