☰
Camera HAL configureStreams反复配置失败?第5次调用缓冲区泄漏问题排查实录
2026/10/20 17:58:50 网站建设 项目流程

1. 先从调用链说起:configureStreams 在上层和 HAL 之间到底扮演什么角色

1.1 一次打开相机到第一次出图的完整链路

做过 Camera HAL 开发的朋友应该都有同感:configureStreams这个名字看起来只是“配置一下流”,实际却是从 App 到 HAL 之间最容易被卡住、也最容易出幺蛾子的环节。前几天我就在某移动芯片平台的 Camera HAL 上排查了一个“第 5 次 configureStreams 失败”的问题,排查过程挺典型,这里把完整思路和踩坑记录整理出来,希望对正在跟流配置较劲的人有帮助。

先快速过一遍调用链,方便不熟悉 HAL3 流程的读者建立上下文。当 App 调用CameraDevice.createCaptureSession时,Camera Framework 会收集所有的输出 Surface(预览 Surface、录像 Surface、ImageReader 等),封装成一个Camera3StreamConfiguration结构体,然后调用到 HAL 的configureStreams接口。HAL 拿到这份配置后,需要做几件最关键的事:

  • 遍历所有 stream,逐个检查 format、宽高、usage flag 是否合法,是否被当前 sensor 和 ISP 支持;
  • 决定每个 stream 对应的 buffer 数量(max_buffers),并申请必要的内部 buffer;
  • 根据当前配置切换底层 ISP/sensor 的工作模式,比如单摄预览、双摄虚化、高像素拍照等;
  • 把所有 stream 的 buffer 句柄和格式信息缓存下来,供后续每个 request 使用。

从上层视角看,configureStreams只是“把 Surface 列表传给 HAL 然后等一个返回值”。但对 HAL 来说,这一步基本等同于正式开工前的总动员:所有硬件资源、buffer 通路、sensor 输出尺寸都要在这里定下来。一旦这里返回错误,上层就直接抛异常,App 端表现通常是黑屏、闪退或者提示“相机无法连接”。

1.2 为什么 HAL 必须“自己决定”流配置而不是照单全收

很多刚从上层转过来的开发会问:App 都指定好宽高了,HAL 照着配置不就行了,为什么还要做那么多校验和重排?原因很简单:App 提交的 Surface 参数是“需求”,不是“能力”。HAL 才是那个知道 sensor 实际输出能力、ISP 带宽、内存带宽的人。

举个例子,App 可能同时要求 1080p 预览 + 4K 录像 + 12MP 拍照三个 stream。sensor 本身只有一种原生输出分辨率,要么是 12MP 全尺寸,要么是 1080p 裁剪。此时 HAL 必须决定:sensor 出 12MP,然后 ISP 做 downscale 给预览和录像用;还是 sensor 出 1080p,拍照时再切回全尺寸。这个决策直接影响功耗、流畅度和拍照延迟,只能由 HAL 根据平台能力来做。

再比如usage flag。上层传下来的GRALLOC_USAGE_HW_CAMERA_ZSL、GRALLOC_USAGE_HW_VIDEO_ENCODER、GRALLOC_USAGE_HW_IMAGE_ENCODER这些标志,决定了 buffer 是给 preview、video encoder 还是 JPEG encoder 消费。HAL 需要根据这些 usage flag 判断 buffer 走哪条硬件通路,是否需要额外申请 internal buffer 做格式转换。如果 HAL 只是简单透传,那底层 ISP 根本不知道该把数据送到哪里。

这就是为什么这次调试中,我一开始没有急着看 HAL 代码,而是先拉通了整条链路,把“上层传了什么”“HAL 收到了什么”对齐。后面才发现,问题恰恰出在一个不常被注意的状态残留上。

2. 某平台 HAL 的流配置逻辑:从 stream 解析到 buffer 落地的关键细节

2.1 进入 configureStreams 后 HAL 都做了哪些校验

我调试的这个平台,HAL 的configureStreams实现大体分为三个阶段。第一阶段是参数解析和合法性检查,第二阶段是 buffer 预算计算和分配,第三阶段是底层硬件模式切换。

第一阶段里,HAL 会先拿到Camera3StreamConfiguration,逐项检查:

  • 每个 stream 的stream_type必须是CAMERA3_STREAM_OUTPUT或CAMERA3_STREAM_INPUT,不允许出现非法类型;
  • format 必须属于平台支持的白名单,比如HAL_PIXEL_FORMAT_YCbCr_420_888、HAL_PIXEL_FORMAT_BLOB、HAL_PIXEL_FORMAT_RAW16;
  • 宽高不能超过 sensor 最大输出尺寸,也不能小到低于 ISP 缩放极限;
  • 对于 blob 格式(JPEG),还要额外核对是否是拍照场景,且 size 是否足够容纳 JPEG 头信息和 EXIF。

第二阶段是 buffer 预算。每个 stream 的max_buffers不是 App 定的,而是 HAL 根据 stream 类型和底层消费方式算出来的。我这次调试的平台上,规则大致是:

stream 类型典型 max_buffers说明
Preview4 ~ 6需要给 preview 队列预留足够深度的 buffer,防止掉帧
Record6 ~ 8video encoder 消费速度慢,需要更多 buffer 周转
Snapshot (BLOB)2 ~ 4拍照后需要 buffer 保存 JPEG 数据,同时要等 encoder 读取
ZSL8 ~ 10环形 buffer 池,需要持续缓存多帧 RAW/YUV 数据

这个预算不是拍脑袋定的,而是和 ISP 的输出帧率、内存带宽、encoder 消费速度强相关。buffer 配少了,高帧率下会频繁出现 buffer 耗尽导致掉帧;配多了,内存占用暴涨,低内存场景直接 OOM。平台通常会在 bringup 阶段用压力测试调出一组经验值,再写死在 HAL 里。所以如果你改动了 sensor 分辨率或帧率,一定要同步评估 buffer 预算是否需要调整。

第三阶段是底层模式切换。HAL 根据 stream 列表里是否包含 BLOB 格式的 stream、是否有 ZSL usage,决定 sensor 工作在普通模式还是 ZSL 模式。这个模式切换往往涉及 sensor 重新 power 序列、ISP 重新初始化,耗时可能达到几十毫秒,所以上层通常会尽量避免频繁调用configureStreams。

2.2 buffer 分配和内部流转:configureStreams 不只是“记参数”

很多人以为configureStreams只是把 stream 信息存下来就返回了,但实际上 HAL 在这个阶段还会做不少 buffer 相关的工作。

以我调试的平台为例,HAL 在configureStreams中会针对每个 stream 做以下事情:

  1. 调用 gralloc 接口申请生产端 buffer,填充到 stream 的 buffer 环形队列里;
  2. 对于 preview/record 这类 YUV 流,如果 sensor 输出格式和 stream 要求的格式不一致(比如 sensor 输出 NV12,但上层要求 YCbCr_420_888),HAL 需要额外申请 internal buffer,后续在 ISP 模块里做格式转换;
  3. 对于 ZSL stream,HAL 会申请一个较大的 buffer 池,并把 sensor 的输出直接写入这个池子,形成“边拍边存”的环形缓存;
  4. 如果平台支持多 camera 并发(比如前后双摄、画中画),configureStreams阶段还会做 camera 间资源仲裁,确认 ISP 或 sensor 资源是否足够。

这些工作直接影响后续 request 处理时的 buffer 获取速度。如果configureStreams阶段 buffer 准备不充分,等真正跑 request 时再去申请,就会出现掉帧和超时。

这一步还有一个容易被忽略的细节:configureStreams被调用时,HAL 应该先把之前配置的旧 buffer 释放掉。但释放时机很有讲究。如果旧 buffer 还有 request 正在使用,贸然释放会导致 use-after-free。所以我这次排查的“第 5 次配置失败”,就是从 buffer 释放和状态复位这个地方开始露出端倪的。

2.3 反复配置时最容易踩的状态残留问题

连续多次调用configureStreams的场景在实际使用中非常常见。比如用户打开相机预览,切到录像,再切回预览,再打开拍照,这个过程中上层可能基于不同的 Surface 组合反复配置。而 HAL 每次进入configureStreams时,都应该像一个干净的二次启动一样,把上一次残留的 stream 信息清干净。

但实际调试中我发现,这个平台某些版本的 HAL 在连续配置时,旧 stream 的 buffer 并没有被完全释放,而是缓存在一个“待释放”列表里,等待所有使用该 buffer 的 request 结束之后再异步释放。这个机制本身没毛病,问题出在“待释放”列表没有做上限保护。如果连续多次配置,旧 buffer 大量堆积,内存占用越来越大,到第 5 次配置时,新 buffer 申请因为内存不足直接失败,返回-ENOMEM。

这个问题在低内存设备上尤其明显。内存充足时可能要到第 10 次、第 20 次才触发;内存紧张时,第 3 次就可能出问题。所以调试时不能只看“第 5 次”,要关注是不是存在“配置次数越多,内存越少”的趋势。

3. 调试过程实录:从 log 定位到“第 5 次配置失败”的根因

3.1 如何抓取并打开 HAL 内部日志

遇到 configureStreams 问题,第一步永远是把 HAL 的 debug log 打开,别指望一次就能从 crash backtrace 看出名堂。这个平台的做法是通过系统属性控制 HAL log 等级:

adb shell setprop vendor.camera.hal.debug 5 adb shell setprop persist.vendor.camera.debug.log 1 adb shell setprop camera.hal.debug 4 adb shell stop vendor.camera-provider adb shell start vendor.camera-provider

设置之后,logcat 里 HAL 相关的日志会明显变多。重点过滤这些 tag:

adb logcat -c adb logcat -v threadtime | grep -E "CamX|CamHAL|SprdCamera|configureStreams|Stream" > hal_log.txt

如果你的平台和我不一样,tag 名会不同,但搜索configureStreams、Stream、以及 HAL 模块名这几个关键词基本都能命中目标。注意 logcat 的输出量可能很大,最好先集中在复现步骤附近抓取,不要从头抓到尾再大海捞针。

日志抓下来之后,我习惯先搜三个东西:第一次configureStreams的进入日志、最后一次失败时的错误码、以及所有alloc buffer/free buffer的日志。这三组日志基本能把问题范围框定出来。

3.2 复现步骤:为什么偏偏是“第 5 次”

这次问题复现路径很固定:打开相机预览,切到录像,再切回预览,再切到录像,如此反复。前 4 次配置都正常,第 5 次点击录像时相机画面卡住,随后上层报错,HAL 返回configureStreams失败。

我最初怀疑是某个 stream 格式或分辨率在第 5 次时变了,导致 HAL 校验不过。所以我在 HAL 里加了临时日志,把所有 stream 参数完整打印出来:

for (int i = 0; i < stream_list->num_streams; i++) { camera3_stream_t *s = stream_list->streams[i]; ALOGI("configureStreams stream[%d]: fmt=%d w=%d h=%d usage=0x%x max_buffers=%d", i, s->format, s->width, s->height, s->usage, s->max_buffers); }

对比第 1 次和第 5 次日志,stream 的参数完全一样,格式、宽高、usage 都没变。这说明问题不是参数校验,而是 HAL 内部资源状态出了问题。

继续往后面搜,发现了线索:第 5 次进入 configureStreams 后,HAL 在分配 preview stream 的 buffer 时失败,返回-ENOMEM。而分配之前,日志里出现了大量“waiting for inflight request done”和“release stream buffer delayed”的信息。也就是说,前面的配置产生的旧 buffer 全部堆积在待释放列表里,一直没有真正 free。

3.3 日志逐段拆解与分析

把 log 按时间线展开,几个关键节点的日志大致是这样:

01:02:23.458 1234 5678 I CamX: configureStreams, num=3 01:02:23.459 1234 5678 I CamX: stream[0]: fmt=0x23 w=1920 h=1080 usage=0x103 01:02:23.459 1234 5678 I CamX: stream[1]: fmt=0x21 w=1280 h=720 usage=0x903 01:02:23.460 1234 5678 I CamX: stream[2]: fmt=0x20 w=4000 h=3000 usage=0x100 01:02:23.461 1234 5678 I CamX: trying to allocate 6 buffers for stream[0] 01:02:23.462 1234 5678 I CamX: try alloc internal buffer +3 01:02:23.463 1234 5678 i Gralloc: 28 buffers in use, system low memory threshold 24 01:02:23.464 1234 5678 E CamX: buffer allocation failed, retry after flushing

最后一行的“retry after flushing”是关键。这说明 HAL 自己已经意识到可能是旧 buffer 没释放导致内存不够,于是尝试先 trigger 一次内部 flush,把 inflight request 强制结束,再释放旧 buffer。但问题在于:如果 flush 之后新 buffer 还是分配失败,HAL 就会直接返回错误。

把日志往更早翻,确认了“28 buffers in use”这个数字从第 3 次配置开始就一路上涨,第 5 次达到了 28,而平台内存水位线的 buffer 上限是 24。等于说前几次配置积累的未释放 buffer 已经超过阈值,新 buffer 自然申请不出来。

这个根因清楚之后,修复方向就很明确了:不是在分配逻辑里加 retry,而是要让旧 buffer 在 configureStreams 进入时尽量提前释放,并且给“待释放列表”加上限,防止无限堆积。

3.4 修复思路与关键代码示意

我把修复分成两步。第一步是强制在进入 configureStreams 时,如果已无 inflight request,立即释放所有带释放列表中的 buffer;如果还有 inflight,则先 trigger flush,阻塞等待全部 request 结束后再释放。第二步是给待释放列表加水位线,超过 N 个就强制等待释放,不再缓存。

伪代码如下:

status_t MyCameraDevice::configureStreams(...) { // Step 1: ensure all old buffers are reclaimed before new allocation waitForInflightRequestDone(); releasePendingBuffers(); // Step 2: check pending buffer queue size, force drain if too large if (pendingBufferList.size() > kMaxPendingBufferCount) { flushInternal(); releasePendingBuffers(); } // Step 3: parse and validate stream list status_t ret = validateStreamList(stream_list); if (ret != OK) { ALOGE("configureStreams: invalid stream list, ret=%d", ret); return ret; } // Step 4: allocate buffers for new streams for (auto &s : stream_list->streams) { ret = allocateStreamBuffers(s); if (ret != OK) { ALOGE("configureStreams: allocate buffer failed, ret=%d", ret); return ret; } } return OK; }

在真实修复中,我并没有直接提交这段伪代码,而是根据平台代码风格做了适配:把releasePendingBuffers提前到解析参数之前,同时把待释放 buffer 列表的长度从“无限增长”改为“最多缓存 4 组配置的 buffer”。这样即使上层连续切换几十次相机模式,内存也不会无限制增长。

这个修复上线后,我连续跑了 50 轮“预览 -> 录像 -> 预览 -> 拍照”的循环,第 5 次配置失败的问题没有再出现。同时用内存监控确认了 buffer 占用稳定在正常水位,没有继续上涨。

4. 常见 configureStreams 问题速查与排查经验

4.1 每种错误码背后对应什么现场

configureStreams返回的错误码就那么几种,但同一个错误码背后的原因千奇百怪。我把这次排查和其他几次遇到的典型情况整理成表格,方便大家对照排查:

错误码直接原因典型现场排查重点
-EINVAL参数不合法上层调用会直接抛IllegalArgumentException对照上层传入的 format/宽高/usage,确认是否在 HAL 支持范围内
-ENOMEMbuffer 分配失败相机画面卡住,然后黑屏退出查内存水位、待释放 buffer 堆积情况、gralloc 分配日志
-EINVALstream 组合不合法比如同时请求 4K 预览 + 4K 录像 + 12MP 拍照确认 ISP 带宽是否足够,sensor 是否能支撑多路不同分辨率输出
-EINVALformat 与 usage 不匹配常见于 ZSL stream 给了 BLOB format检查上层是否用错了 ImageReader 的 format
-ENODEV底层硬件初始化失败HAL 日志里伴随 sensor/ISP error查 sensor 驱动、ISP 固件加载日志,确认上电时序是否正常
-EINVALbuffer 数超上限上层传的 max_buffers 比 HAL 允许的大注意 HAL 会忽略上层传的 max_buffers,但如果校验代码有 bug 也会报错

这张表只是帮你快速定位方向,具体还要结合 HAL 的私有 log。我的经验是,先看错误码,再看 HAL 内部日志最后十行,基本能分清是“参数问题”“内存问题”还是“硬件问题”。

4.2 配置前必做的三个“强迫症检查”

排查 configureStreams 的问题,我个人的习惯是,不管 bug 表现是多奇怪,先做以下三个检查,能排除掉大概一半的坑。

第一个检查是usage flag 是否被正确传递。上层Surface的 usage flag 经过 Camera Framework 传递到 HAL 时,比较容易出现标志位丢失或追加的情况。最常见的是 ZSL 模式需要的GRALLOC_USAGE_HW_CAMERA_ZSL标志丢失,导致 HAL 没有进入 ZSL 工作模式。排查方法是在 configureStreams 入口把每个 stream 的 usage 打印出来,和上层ImageReader配置对比。

第二个检查是宽高是否交换。这个问题在竖屏 App 上特别常见。App 拿到的是旋转前的 Sensor 方向,HAL 得到的是经过 framework 换算的宽高。如果 HAL 里没有做正确的方向换算,会出现 1920x1080 变成了 1080x1920,然后 ISP 不支持竖屏输出,报-EINVAL。有些 HAL 自己会处理 orientation,但有些平台要求上层在 createSession 时就传入已经旋转好的尺寸,这个约定不搞清楚就会踩坑。

第三个检查是camera 打开数量是否正确。多 camera 并发场景下,第二个 camera 进入 configureStreams 时,HAL 可能需要和第一个 camera 共享 ISP 资源。如果资源仲裁没有做,或者资源计数没有正确累加,第二个 camera 的 configureStreams 会间歇性地失败。而且这个失败往往不是必现的,和第一个 camera 当前的工作负载有关,排查起来很头疼。建议这种场景先把并发 camera 打开的日志对齐,确认资源仲裁是否生效。

4.3 一批好用的调试手段和厂商工具

除了加日志,还有几个调试手段在 configureStreams 问题上特别有效。

第一个是CTS/VTS 测试用例。Android 的 Camera ITS 测试里有大量覆盖 configureStreams 的用例,比如test_sensor_fusion、test_continuous_capture,这些用例会反复以不同尺寸和格式组合创建 capture session。如果你的平台能跑通这些用例,绝大部分 configureStreams 的合法性问题都已经被覆盖了。跑用例时记得抓 full log,不要只看 pass/fail。

第二个是camera HAL 的私有 debug 节点。很多平台在 sysfs 或 debugfs 下会有 sensor/ISP 的实时状态节点,比如当前 sensor 输出模式、ISP 带宽占用、buffer pool 使用率。我之前在某平台上遇到过明明 HAL 配置都是对的,但 sensor 就是切不过去,最后通过 debugfs 看到一个寄存器状态一直停留在旧模式下,才知道是 sensor 驱动没有正确响应切换命令。

第三个是我个人很推荐的做法:把 configureStreams 的入参和出参做成一个独立 dump 文件,每次配置都追加写入,而不是只靠 logcat。logcat buffer 会被其他日志冲掉,而且 logcat 在 crash 之后可能丢失现场。写 dump 文件的代价很小,但在排查“第 N 次偶现”这类问题时价值巨大。我当时就是靠这个 dump 文件确认了第 5 次配置时参数没有变化,才把目光从参数校验转移到 buffer 管理上。

5. 写在最后的排查心得

这次“第 5 次 hal configureStreams 失败”的问题,从发现到定位花了大概半天,真正修复只改了几行代码。整个过程下来,我最大的感受是:configureStreams 的问题很少是孤立出现的,多数都是状态管理的问题——某个 buffer 没释放、某个模式没复位、某个计数没清零。而这些状态问题又特别隐蔽,因为前几次配置往往都正常,等到资源耗尽才突然暴露。

有个小技巧很实用:如果遇到的是“第 N 次才失败”的偶现问题,别急着在 HAL 里加各种判断,先写一个每次配置都会执行的 dump 函数,把内存占用、待释放 buffer 数量、当前 stream 列表完整记录下来。连续复现几次,对比第 1 次和第 N 次的差异,往往一眼就能看到是哪个资源在持续累积。

另外也提醒一句,有些厂商 HAL 里自带的 retry 逻辑(比如我这次看到的 retry after flushing)并不是万能的。它可能掩盖问题,让前几次配置看起来正常,但真正到资源耗尽那一刻,一次性失败照样会冒出来。遇到这种代码,要顺着它把底层的资源占用逻辑抠透,而不是简单把 retry 次数加大。

configureStreams 这个接口说简单也简单,说复杂也复杂。它不像 request 循环那样每帧都在跑,但一旦出了问题,影响的直接是“相机能不能打开”这个最基础的用户体验。把流配置这关把好了,后面预览、拍照、录像的稳定性都会省心很多。

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

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

立即咨询