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 做以下事情:调用 gralloc 接口申请生产端 buffer,填充到 stream 的 buffer 环形队列里;对于 preview/record 这类 YUV 流,如果 sensor 输出格式和 stream 要求的格式不一致(比如 sensor 输出 NV12,但上层要求 YCbCr_420_888),HAL 需要额外申请 internal buffer,后续在 ISP 模块里做格式转换;对于 ZSL stream,HAL 会申请一个较大的 buffer 池,并把 sensor 的输出直接写入这个池子,形成“边拍边存”的环形缓存;如果平台支持多 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 循环那样每帧都在跑,但一旦出了问题,影响的直接是“相机能不能打开”这个最基础的用户体验。把流配置这关把好了,后面预览、拍照、录像的稳定性都会省心很多。
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 做以下事情:调用 gralloc 接口申请生产端 buffer,填充到 stream 的 buffer 环形队列里;对于 preview/record 这类 YUV 流,如果 sensor 输出格式和 stream 要求的格式不一致(比如 sensor 输出 NV12,但上层要求 YCbCr_420_888),HAL 需要额外申请 internal buffer,后续在 ISP 模块里做格式转换;对于 ZSL stream,HAL 会申请一个较大的 buffer 池,并把 sensor 的输出直接写入这个池子,形成“边拍边存”的环形缓存;如果平台支持多 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 循环那样每帧都在跑,但一旦出了问题,影响的直接是“相机能不能打开”这个最基础的用户体验。把流配置这关把好了,后面预览、拍照、录像的稳定性都会省心很多。