4、Request/Stream内存模型:Camera3的Request与Stream关系,Stream配置与Buffer需求计算,Buffer的预留与复用策略

好,咱们今天聊聊Camera HAL里最核心的内存模型。说白了,就是Request和Stream怎么配合,Buffer怎么算、怎么留、怎么复用。这部分要是搞不清楚,后面调试起来会非常痛苦。我自己就踩过不少坑,今天一并分享给你。

4.1 Request与Stream的关系:一对多的舞蹈

先理清基本概念。在Camera3架构里,一个Request对应一次拍照或一帧预览。但一个Request可以包含多个Stream的输出。什么意思呢?

你按下快门,可能同时需要:

  • 一张全尺寸的JPEG照片
  • 一张缩略图
  • 一帧预览画面

这三个输出,就是三个不同的Stream。它们都挂载在同一个Request下。我刚开始接触时,总觉得一个Request只对应一个输出,后来发现完全不是这么回事。

核心关系:Request是时间轴上的一个节点,Stream是并行的数据管道。一个Request可以同时向多个Stream提交Buffer请求。

举个例子:

// 一个Request对应三个Stream
request->streams[0] = stream_preview;   // 预览流
request->streams[1] = stream_snapshot;  // 拍照流
request->streams[2] = stream_thumbnail; // 缩略图流

每个Stream都有自己的Buffer队列。HAL需要从每个Stream的队列里取出Buffer,填好数据,再还回去。嗯,这里要注意:不同Stream的Buffer大小、格式、使用方式都可能不同

4.2 Stream配置与Buffer需求计算

Stream怎么配?这直接决定了你要多少内存。我见过不少团队,上来就配最大的分辨率,结果内存爆了。其实,算清楚Buffer需求并不难。

关键参数就这几个:

参数 说明 影响
width × height 分辨率 线性增长
format 像素格式(NV12、YUV420等) 每像素字节数不同
usage 使用标记(GRALLOC_USAGE_HW_*) 决定分配策略
max_buffers 最大Buffer数 乘数效应

Buffer大小的计算公式很简单:

// 以NV12为例
size = width * height * 3 / 2;  // Y平面 + UV平面

// 实际分配时还要考虑对齐
stride = ALIGN(width, 32);
size_aligned = stride * height * 3 / 2;

我曾经在一个项目里,直接用了width而不是stride去算大小,结果分配出来的Buffer不够用,导致ISP写数据时越界。那个bug查了我整整两天。所以,一定要用stride,别用width

我的习惯:在Stream配置阶段,就把每个Stream的stride、size、count全部算好,做成一张表。后面分配Buffer时直接查表,既快又不容易出错。

Buffer数量怎么定?一般来说:

  • 预览流:3-4个Buffer就够了。多了浪费,少了容易丢帧。
  • 拍照流:2-3个Buffer。因为拍照不连续,不需要太多。
  • 视频流:4-6个Buffer。编码器那边可能还会多占几个。

但这不是绝对的。我记得有一次做高帧率录像,120fps,预览流配了4个Buffer,结果还是偶尔卡顿。后来加到6个,问题就解决了。说白了,Buffer数量跟帧率和处理延迟强相关,得实测调整。

4.3 Buffer的预留与复用策略

Buffer怎么留、怎么复用,这是内存优化的重头戏。我见过两种极端:一种是每次Request都重新分配Buffer,内存碎片严重;另一种是只分配一次,死活用到底,结果灵活性很差。

我的建议是:预留 + 按需复用

4.3.1 预留策略

在Stream配置阶段,就把所有可能用到的Buffer都分配好。比如:

// 预留Buffer池
typedef struct {
    buffer_handle_t *buffers[MAX_BUFFERS];
    int count;
    int in_use[MAX_BUFFERS];
} buffer_pool_t;

// 初始化时全部分配
for (int i = 0; i < pool->count; i++) {
    alloc_buffer(&pool->buffers[i], width, height, format);
    pool->in_use[i] = 0;
}

这样做的好处是:运行时没有分配延迟,不会因为分配失败导致丢帧。坏处是:内存占用固定,不能动态调整。

注意:预留的Buffer数量要留有余量。我曾经在一个项目里,按最大需求预留了Buffer,结果某个场景下需求突然增加,直接导致分配失败。后来我改成预留1.5倍,才彻底解决。

4.3.2 复用策略

Buffer复用,说白了就是同一个Buffer在不同Request之间循环使用。关键是怎么管理好“谁在用、谁空闲”。

我常用的方法是:

  1. 空闲队列:存放当前可用的Buffer。
  2. 使用中队列:存放正在被HAL或ISP使用的Buffer。
  3. 完成队列:存放已经填好数据、等待返回给Framework的Buffer。

代码大概长这样:

// 从空闲队列取Buffer
buffer_handle_t *get_buffer(buffer_pool_t *pool) {
    for (int i = 0; i < pool->count; i++) {
        if (!pool->in_use[i]) {
            pool->in_use[i] = 1;
            return pool->buffers[i];
        }
    }
    return NULL;  // 没有空闲Buffer,需要等待
}

// 使用完后归还
void put_buffer(buffer_pool_t *pool, buffer_handle_t *buf) {
    for (int i = 0; i < pool->count; i++) {
        if (pool->buffers[i] == buf) {
            pool->in_use[i] = 0;
            return;
        }
    }
}

你想想看,如果每次Request都重新分配Buffer,那内存分配器会频繁调用,碎片越来越多。而复用策略,相当于把内存管理权握在自己手里,可控性高得多。

4.3.3 特殊情况处理

有些场景下,Buffer不能简单复用。比如:

  • 分辨率变化:从1080p切换到4K,原来的Buffer大小不够,必须重新分配。
  • 格式变化:从NV12切换到YUV420,Buffer布局不同,不能复用。
  • 使用标记变化:比如从CPU访问切换到GPU访问,可能需要重新分配。

遇到这些情况,我的做法是:先释放旧Buffer池,再分配新Buffer池。千万别想着原地修改,容易出问题。

一个小技巧:在Stream配置变更时,不要立刻释放所有Buffer。先分配新的,等新Buffer到位后再释放旧的。这样可以避免中间状态没有Buffer可用。

4.4 总结一下

Request/Stream内存模型,说白了就是三件事:

  • 关系搞清楚:一个Request可以挂多个Stream,每个Stream有自己的Buffer队列。
  • 需求算明白:用stride算大小,根据场景定数量,留有余量。
  • 策略执行好:预留Buffer池,复用空闲Buffer,特殊情况特殊处理。

嗯,这部分内容就到这里。下一章咱们聊聊更具体的Buffer管理技巧,包括怎么处理跨进程传递、怎么避免内存泄漏。到时候再细说。