做 Camera 调试这几年,我一直觉得 metadata 是最容易被忽略、却又最值得花时间啃的一块。很多人拿到 Camera HAL 的代码,第一反应是追 buffer 流向,第二反应是看 3A 算法,很少有人会专门坐下来把 metadata 的来龙去脉捋一遍。但实际项目里,像“preparing metadata 卡住”“VTS 报错”“某个 AI 相机的 model metadata 缺失退回 fallback”这类问题,追到最后几乎都会落到 metadata 的处理上。这篇就专门聊聊 Camera 系统里的 metadata——我从概念、buffer 管理、异常排查到性能调优,把该说的都说一遍。
我默认看这篇的朋友已经接触过 Camera 驱动或者 HAL 层开发,至少知道 Camera 的大致数据通路。如果你刚入行,也不用慌,我会把基础概念讲得足够清楚,后面排查和调优的部分你收藏起来,等项目里真踩到坑了再翻出来对照。
1. 先搞清楚 metadata 到底是什么
1.1 一个请求一个结果,中间全靠 metadata 沟通
Camera 系统里最常见的误解,是把 metadata 当成“拍照参数”或者“EXIF 信息”。其实 metadata 的覆盖面远不止这些。在 Android Camera 框架里,metadata 本质上是一套 key-value 结构,用来描述一次图像采集的全部上下文,包括 Sensor 的曝光时间、ISO、光圈、对焦位置、3A 状态、镜头畸变校正参数、降噪强度、裁剪区域、时间戳、人脸检测结果,甚至多摄设备上每个物理摄像头各自的标定参数。
一次完整的拍照流程可以这样理解:上层(App 或者 CameraService)向 HAL 发一个 capture request,这个 request 里带着一堆 metadata,告诉 HAL“我想要什么样的画面”;HAL 处理完之后,除了返回图像 buffer,还会返回一份 corresponding result metadata,告诉上层“我实际上拍成了什么样”。一来一回,request metadata 和 result metadata,构成了 Camera pipeline 里最核心的信息通道。
有意思的是,这个通道并不只在拍照时才有。预览模式下,每一帧 Stream 的配置、每一帧 buffer 的 return fence 信号、每一帧 3A 状态的更新,全部都要靠 metadata 来携带。所以你可以把 metadata 理解成整个 Camera 系统的“神经信号”——它不直接产生图像,但没有它,图像的曝光、对焦、色彩全是乱的,buffer 的同步和分发也会彻底失控。
1.2 静态metadata、动态metadata、请求metadata各管什么
按作用范围和时间尺度,Android Camera 的 metadata 可以分为三类,搞清这三类,很多代码逻辑就顺了。
第一类是静态 metadata(Static metadata)。它在 CameraID 对应的 HAL 设备打开那一刻就固定下来,描述的是这颗摄像头的物理能力,比如支持的最大分辨率、可用的 AE 模式列表、镜头的最小对焦距离、像素大小、传感器朝向等。App 通过CameraCharacteristics拿到的就是这些信息。这类 metadata 由 HAL 在getCameraCharacteristics回调里一次性返回,之后不会变化。
第二类是动态 metadata(Dynamic metadata)。它描述的是当前状态或最近一次 capture 的结果,比如当前帧的曝光时间、ISO、对焦距离、3A 是否收敛、当前帧时间戳等。上层通过CameraCaptureSession.CaptureCallback的onCaptureCompleted拿到的是这类。每次 capture 完成都会更新,实时性要求高。
第三类是请求 metadata(Request metadata)。这是上层在每次 capture request 里主动设置的,比如手动曝光时指定 exposure time 和 sensitivity,手动对焦时指定 focus distance,或者限制帧率时指定 target FPS range。它影响的是“这次要拍成什么样”。
这三类加在一起,才构成了完整的信息环。很多刚接触 HAL 的人容易犯的错是,把动态 metadata 当成静态的缓存在某个全局变量里,结果预览帧率一变,缓存的数据和实际帧就对不上了。
1.3 一个经典场景:3A 状态和 AE 结果怎么通过 metadata 流转
举个最典型的场景——自动曝光(AE)。App 发起预览后,HAL 收到一个带CONTROL_AE_MODE_ON的 request metadata,然后 3A 算法开始计算当前环境亮度,得到一组新的曝光参数(exposure time、gain、frame duration)。这些参数又会通过 result metadata 里面的SENSOR_EXPOSURE_TIME、SENSOR_SENSITIVITY上报给框架。框架拿到后,一方面反馈给 App 显示,另一方面把这些参数存储起来用于后续 frame 的同步。
实际调试里最常见的诡异现象是:画面亮度一直在跳,但 HAL 代码里明明把曝光参数写死了。查到最后发现,是 request metadata 里的CONTROL_AE_MODE被上层写成了OFF,而 HAL 的 AE 算法分支没走到,直接走了固定参数分支。这类问题靠日志很难一眼看出来,因为 HAL 跑得很快,参数本身也没有报错。我的排查习惯是,先在 HAL 入口处把每次 request metadata 的关键 tag 全量 dump 出来,做一次前后帧对比,再去看 3A 的行为。这比在算法里加断点高效得多。
2. 从 request 到 result:metadata 在多媒体 buffer 管理里的关键作用
2.1 metadata 和 buffer 是绑在一块走的
很多从事上层开发的朋友会问一个问题:“metadata 和图像 buffer 到底什么关系?”答案是:它们绑在一块走。一次 capture 的核心产物有两样,一个是填充好的图像 buffer,一个是描述这个 buffer 的 metadata。两者通过同一个 frame number 关联起来,HAL 层的 buffer manager 负责同步它们的生命周期。
具体到代码上,HAL 3.x 的processCaptureRequest会传入一个capture_request,里面有num_physcam_settings、settings、input_buffers、output_buffers等字段。其中settings指向的就是 request metadata 的内存地址,output_buffers指向的是一个stream_buffer数组。HAL 处理完每一路 stream 后,需要调用processCaptureResult,把 result metadata 和对应的 output buffer 一起交还。
如果 HAL 把 metadata 和 buffer 的返回节奏搞错了,问题会非常严重。比如 camera framework 的Camera3Stream里,会通过buffer_producer接口去等待 HAL 返回的 buffer 和 metadata。如果你的 HAL 在某个 stream 上只返回了 metadata 但没返回 buffer,或者反过来只返回 buffer 但 metadata 的 frame number 对不上,轻则预览黑屏,重则整个 Camera HAL 直接崩掉,系统报camera HAL: processCaptureResult: Buffer ... has already been returned之类的错误。
2.2 buffer 管理中的 metadata 同步:fence 和时间戳
buffer 同步是 metadata 发挥作用的核心场景之一。以 Android 的 buffer management 为例,每一路 output buffer 在 HAL 内部其实对应一个buffer_handle_t,并且有一个 acquire fence 和 release fence。acquire fence 表示 producer 什么时候可以开始写数据,release fence 表示 consumer 什么时候可以开始读数据。而 metadata 里有一个 tag 叫SENSOR_TIMESTAMP,它标明了 buffer 里图像数据的采集时刻。
为什么这个时间戳这么重要?因为在多路 stream 的场景下(比如同时输出预览流、拍照流、深度流),每一路 buffer 的填写速度不一样,如果没有一个共同的“采集时刻”来对齐,上层在组合这些流的时候就会出现时间错位。比如预览流已经跑到第 30 帧,拍照流才到第 12 帧,合成的结果就会很奇怪。SENSOR_TIMESTAMP就是用来干这个的。上层拿到 buffer 和 metadata 后,会按照时间戳把同一时刻的数据归到一组。
我见过不少 HAL 实现里,时间戳直接用systemTime()来打,这在单摄场景下可能看不出问题,但一旦上了多摄,两颗 sensor 各自的启动时间、曝光时间不一样,如果你不用同一个时钟源去取时间戳,左右两颗摄像头给出的同一物理时刻的 timestamp 可能会差出好几毫秒,深度合成出来的图就会出现边缘撕裂。
正确做法是,所有物理摄像头的时间戳都基于同一个 boottime 时钟去取,并且把SENSOR_TIMESTAMP的时钟域在 HAL 里统一换算。这个点,在 VTS 的CameraMetadataTest里也有对应的测试用例,专门检查时间戳是否单调递增。
2.3 从 stream 配置到 metadata 空间分配:preparing metadata 卡住怎么办
这些热词里有一条“preparing metadata 卡住”,这个印象很深。如果你是做上层应用开发的,你大概率在camera2api 的sessionConfiguration之后、onConfigured回调触发之前遇到过“画面一直黑着,log 停在 preparing metadata”的情况。
这个“preparing metadata”其实不是 framework 里的标准字符串,一般在 vendor 的 HAL 里,这行 log 代表的是 HAL 在准备 metadata 的内存空间或者查询 sensor 能力时发生了阻塞。原因通常是下面几个:
第一,metadata 内存分配失败。HAL 在处理 stream configuration 时,需要为每个 stream 分配对应的 metadata buffer,如果内存不足或者分配方式有问题,就会卡住。这种情况在 32 位进程上尤其常见,因为 metadata 的 buffer 本身不小,再加上多个 stream 并发,内存一下就顶满了。
第二,sensor 驱动响应超时。HAL 在准备 metadata 时,会去查询 sensor 的当前状态(比如当前的曝光、增益、温度),如果 sensor 驱动没有按时返回,HAL 就会阻塞在等待状态。这个问题在一些老平台上很常见,sensor 驱动的 I2C 通信不稳定,偶发超时,一旦发生就像“卡住”了。
第三,HAL 自身的死锁。如果你的 HAL 在 stream configuration 阶段持有了某个锁,然后 preparation 里又去请求同一个锁,就会死锁。这种问题最难查,因为 log 看不出明显异常,只能通过抓取debuggerd或者ANRtrace 来看线程栈。
之前排查过一个项目,现象就是切到某个特定分辨率后,预览一直黑屏,log 反复出现preparing metadata。最后是通过 tracing 手段抓线程栈发现的,是 vendor 的某一个回调在占用同一个 mutex,而 HAL 在construct_default_request_settings里又去 acquire 同一个 mutex,导致了死锁。修掉锁的顺序之后,问题就消失了。
3. Camera metadata 的代码级解析:从 HAL 到 VTS 的全面审视
3.1 metadata 的底层存储协议:vendor_tag 与 buffer
到了代码层面,metadata 的存储不是一个普通的哈希表,而是一块连续内存,底层用 vendor 私有的 buffer 结构来管理。Android framework 侧用V3协议管理 metadata,HAL 侧则使用camera_metadata_t结构体和一系列宏/函数来操作它。
在 HAL 里,camera_metadata_t是由camera_metadata.c实现的一棵“tag 到 value”的序列化结构。每个 tag 通过camera_metadata_tag_t枚举值区分。常见的核心 tag 有:
ANDROID_SENSOR_EXPOSURE_TIME和ANDROID_SENSOR_SENSITIVITY:曝光和增益。ANDROID_CONTROL_AE_MODE、ANDROID_CONTROL_AWB_MODE、ANDROID_CONTROL_AF_MODE:3A 控制模式。ANDROID_JPEG_ORIENTATION、ANDROID_JPEG_QUALITY:JPEG 编码相关。ANDROID_STATISTICS_FACE_RECTANGLES:人脸检测结果。ANDROID_LENS_DISTORTION:镜头畸变校正数据。
vendor 也可以定义自己的 tag,通过SYSTEM或VENDOR范围的 tag 值,配合vendor_tag_ops向 framework 暴露。这类 vendor tag 适合传递一些平台特定的参数,比如某个 NPU 单元的开关状态、特定的降噪强度档位。
内存结构上,camera_metadata_t内部有 entry 区、data 区、buffer 区,操作时通过get_camera_metadata_entry或者find_camera_metadata_entry来查找。要注意的是,find_camera_metadata_entry是 O(n) 的,在性能敏感的路径上不要频繁调用,最好在设置阶段就一次性读取需要的 tag 并缓存指针。
3.2 用 vendor_tag 扩展 metadata 的注意事项
如果你要扩展 metadata,有几个坑必须避开。
第一个坑是 tag 值冲突。VENDOR 范围的 tag 值并不是随便定义的,它需要在camera_metadata_tags.h里统一分配,确保和 framework 以及其它 vendor 模块不冲突。如果随手上报一个0x80000000附近的值,轻则 framework 解析不了,重则整个相机服务崩溃。
第二个坑是 media type 的匹配。每个 tag 都对应一种数据类型,比如 BYTE、INT32、INT64、FLOAT、DOUBLE、RATIONAL。你上报的时候数据类型必须严格匹配,否则在解析端会直接出错。我见过一个案例,某个 vendor 把 FLOAT 类型写成了 INT32,拍照数量一多,camera_metadata解析直接断言失败。
第三个坑是 lifecycle 管理。vendor_tag 的注册、查询、释放必须严格对称,framework 在每次 open camera 时动态加载 vendor tag 列表,你如果只注册不释放,时间久了就会有内存泄漏的风险。
3.3 metadata 对 VTS 测试的影响:如何稳定通过 CameraMetadataTest
热词里出现了“camera 摄像头 vts”,这其实就是 Android 的 VTS 测试(Vendor Test Suite)里的 Camera 部分。VTS 对 metadata 有一整套测试用例,比如CameraMetadataTest、CameraDeviceTest、CameraExtensionCharacteristicsTest等。这些测试会枚举设备支持的所有静态 metadata 条目,并验证它们的取值范围、一致性、格式是否正确。
最常见的问题是:android.statistics.lensShadingMapMode设为ON时,却没有提供对应的 map 尺寸;或者android.sensor.info.maxFrameDuration和实际 able 的帧时长不一致。VTS 会直接用CameraMetadata的 API 去反复读取、比对,只要你有一项对不上,整个测试项就会 fail。
我在项目里总结了一条优化路径:在 HAL 实现阶段,把静态 metadata 的填充当作一份“配置清单”来管理,用脚本统一生成,而不是手写在各个文件里。这样最少有一个好处:VTS 里所有需要枚举的 tag,都能确保在清单里能找到对应项。把这份清单和 VTS 测试用例放在一起做自动化比对,发布前就能提前发现问题。
另外一个容易忽略的点是 timeouts。VTS 里每个测试项都有时间限制,如果 HAL 的 metadata 获取过程过慢,比如某个静态 tag 需要去访问 ISP 的驱动寄存器,每次都花几十毫秒,那么测试很容易超时。这个情况在高通平台上我见过多次。最后的解法是,在 HAL 初始化阶段把这类需要查硬件寄存器的信息缓存下来,后续 get 时直接返回缓存,不再实时查询。
3.4 一个具体的 VTS 报错案例分析
我们项目里某次 VTS 测试报了这样的错误:
CameraMetadataTest testCameraCharacteristicsKeys ... FAIL: android.control.aeCompensationStep Expected: rational value in range [-2, 2] Actual: 0.333333一看就知道是某个 HAL 把 AE Compensation 的步长设置成了 1/3 档,但 VTS 要求的取值范围是 [-2, 2],而且步长必须是 2 的幂次分之一(比如 1/2、1/4)。这个设定其实是为了保证 framework 在做 AE 补偿计算时,能够精确地映射到 sensor 的曝光步进。
解决办法很简单,把android.control.aeCompensationStep改成 1/2,或者设成 1/1,只要满足 VTS 要求就行。这个案例提醒我们一个原则:metadata 的取值不能只看业务合不合理,还要看 VTS 的约束。在写 HAL 代码之前,真的很有必要把camera_metadata_tags.h里每个 tag 的注释和 VTS 测试源码过一遍。
4. 与 metadata 相关的疑难杂症:梳理热词里那些让人崩溃的报错
4.1 “model metadata forkimi-k3not found” 是怎么来的
这个热词看起来跟 Camera 没什么关系,像是某个 AI 模型推理框架在加载 metadata 时找不到对应设备的配置,于是报了一句“model metadata not found, defaulting to fallback metadata”。这类问题在 Camera 领域也有,只是报错名字不一样。
AI 相机功能(比如 AI 夜景、AI 场景识别)经常需要加载一个和当前硬件平台绑定的模型包,模型包里面有一份 model metadata,记录模型的输入尺寸、量化参数、支持的 camera id、期望的帧率等。如果这份 metadata 缺失或者 camera id 对不上,加载框架就会用 fallback metadata 强行跑,性能大概率打折,或者根本无法启动。
排查思路很直接:第一步,确认模型包路径下是否有 metadata 文件,文件格式是否符合预期;第二步,确认 metadata 里的 camera id 和当前打开的 CameraID 是否一致;第三步,确认 metadata 里的输入尺寸和 HAL 实际输出的 buffer 尺寸是否匹配。大多数情况下,问题出在第三步——模型期望的输入尺寸跟 AI 相机设置的分辨率不一致,导致推理框架只能退到默认配置。
4.2 next camera 与 metadata:apk 和 framework 之间的配适
“next camera apk”这个热词,看起来是在说某个 Next Camera 或类似的项目打包出来的 APK 里,涉及 metadata 的适配问题。这类 APK 往往会对某些 metadata tag 做特殊处理,比如读取 vendor tag 来控制多帧降噪或者 HDR 模式。如果 framework 和 APK 之间的 metadata 定义不一致,最常见的结果就是:
CameraCharacteristics.get()返回null,因为 framework 里没有这个 tag。CaptureRequest.Builder.set()抛异常,因为 builder 不支持这个 tag。- 打开 Camera 时直接报错,因为某个必填项缺失。
如果你是自己开发 Camera 应用,又喜欢深度控制 HAL 参数,我的建议是:不要用CaptureRequest.Builder去塞一个 framework 不认识的 tag,而是通过 vendor tag 扩展机制去传递。CameraExtensionCharacteristics和CameraExtensionSession这类 API 也是同理,它们对 metadata 的集合有严格的校验,不是随便设置就能过的。
如果因为某种原因必须用自定义 tag,在 APK 里一定要做足够的防御性编码,比如在构建 request 之前,先判断CameraCharacteristics.getAvailableCaptureRequestKeys()里是否包含你的自定义 tag,不包含就降级处理,避免 crash。
4.3 tomcat、yum 这些非 Camera 报错为什么也会混进来
热词里还有“tomcat 启动报错 could not obtain connection to query metadata”和“yum install -y fontconfig mkfontscale errors during downloading metadata for”。这两条虽然不是 Camera metadata,但都属于同一个 metadata 语义——描述数据的数据。
Tomcat 那个报错,本质上是应用配置里用了metadata作为数据库表名或者 JPA 实体名,而 Tomcat 启动时需要查询数据库的连接信息和 schema 元数据,结果连接失败,就报出了 “could not obtain connection to query metadata”。排查的办法就是检查数据库连接配置、驱动、账号权限。
yum 那个报错更简单,是 yum 源里的 metadata 文件下载失败。常见的解决办法是清理 yum 缓存,yum clean all,然后重新yum makecache,再不行就换一个可用源。
我特意把这两条放进来,是想说明一个道理:metadata 这个词汇,在不同的技术栈里含义完全不同,但排查思路是相通的——先搞清楚这个 metadata 是谁在什么时候产生的,谁在消费它,它的生命周期到哪儿结束。只要把这条链路搞清楚了,实在不行就 grep 日志、抓 trace,问题总能定位到。
4.4 “open camera sourceforge”项目能带给我们什么
热词里还有一条 “open camera sourceforge”,指的是 Open Camera 这个开源相机应用。它之所以经常出现在 metadata 相关的讨论里,是因为它对Camera2API 的参数暴露很全,很多拍照模式下会直接操作各种CaptureRequest.Key。
如果你想深入研究 metadata 在各种模式下的实际取值,把 Open Camera 的源码拉下来配合一台 Android 设备跑一遍,在onCaptureCompleted里打日志,观察不同场景(夜景、HDR、连拍)下 metadata 的变化,是非常高效的学习路径。
我自己调试 HAL 时也经常用 Open Camera 做参照——它是纯 Camera2 API 实现,不依赖厂商私有库。遇到“这个参数怎么设才合适”的疑问,我先在 Open Camera 里打开对应的开关,然后抓 HAL 层的 log,看看它到底向 HAL 塞了哪些 metadata。这个方法比翻文档实在得多。
5. metadata 的性能优化与调试技巧
5.1 减少 request/result metadata 的拷贝,性能能涨一个档次
在 Camera HAL 里,metadata 的拷贝是性能杀手。一次 preview 流程里,request metadata 和 result metadata 可能要被拷贝好几次:框架从 App 进程跨 binder 传到 camera provider,provider 再转给 HAL,HAL 处理完再传回来。
这类拷贝虽然单个不大(一般几 KB 到几十 KB),但架不住帧率高一帧一帧累积。有些性能敏感的项目里,每秒 30 帧的预览 + 每秒 30 帧的回调,metadata 的 memcpy 就会占掉不少 CPU。
优化方法有三种,优先级从高到低排列:
第一,能传引用就传引用,不要传值。在 C++ 层用camera_metadata_t*传递,框架层适当使用 move 语义。
第二,只传增量。如果某一路 stream 的参数不发生改变,就不需要每次重复传输所有 metadata,只传变化的部分。Android 的 capture request 机制本身就支持只包含必要 tag 的 partial request。
第三,如果一定要拷贝,用copy_camera_metadata而不是逐个 entry 拷贝,后者效率低到没法看。
5.2 metadata 合并的坑:merge 策略和优先级
在 multi-camera 或者 multi-stream 场景下,经常需要把多个逻辑 camera 的 metadata 合并成一个。这里有个很容易出问题的点——当一个 sensor 报SENSOR_EXPOSURE_TIME,另一个 sensor 也报同样的 tag,最终 result metadata 里以哪个为准?
Android 框架在Camera3Device里有自己的 merge 行为,一般来说,主摄像头的 result 会作为主导,其它 physical camera 的 metadata 会被放到physicalCameraMetadata里。如果你需要拿副摄的曝光参数,不能简单地从主 result 里读,要从 physical metadata 里去取。
我的建议是,在 HAL 层就把物理摄像头的身份信息写到 metadata 里,比如在ANDROID_LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID旁边,把实际成像的物理 camera id 一起填上。这样上层拿到 result 之后,不用靠猜,直接按 physical id 去索引。
5.3 用 dumpsys 和 HAL 层 log 快速定位 metadata 异常
最后分享一套我用了几年的 metadata 调试方法论。
我在需求确认阶段不会直接上手写代码,而是先在设备上跑一遍完整流程,用dumpsys media.camera看一下当前的 Camera 特性:
adb shell dumpsys media.camera | grep -A 5 "Camera ID"这个命令能输出每个 CameraID 的静态 metadata 摘要,很多低级配置错误一眼就能看出来,比如分辨率列表异常、AE 补偿范围不对、曝光时间范围 bug 等。
如果是运行期的问题,我会在 HAL 的processCaptureRequest入口和processCaptureResult出口,把关键 metadata 打印出来,用 tag 名而不是 tag 值,这样在 logcat 里 grep 信息更直观。配合adb shell setprop persist.vendor.camera.logs 0x1之类的 vendor 调试开关,把 HAL 的详细日志打开。
如果是崩溃问题,就用 tombstone 配合 debuggerd 抓堆栈,重点看 metadata 的访问指针是否失效。我曾经定位过一个疑难 bug,就是 HAL 返回的 result metadata 指针,在异步线程里被提前释放了,导致 framework 在解析 metadata 时随机崩溃。那个 trace 一开始根本看不出来问题,是后来在 crash 地址上发现指针值异常地大,怀疑是已经 free 过的堆内存,才用 address sanitizer 复现出来的。
总而言之,metadata 这块在 Camera 系统里就是一个介于硬件和框架之间的转换层,你把它的运行时行为吃透了,遇到问题就不会慌。平时多读camera_metadata_tags.h的注释,多在真机上 dump 各帧的 metadata 对比,积累一些自己的“常见模式”,调试效率会有质的提升。