接手某项目和Camera相关的模块时,我第一次系统地啃Android Camera HAL3。那份接口头文件里全是结构体和函数指针,每个字段都有注释,但注释之间缺的正是"架构"两个字——你从哪进来,该往哪走,数据怎么流转,边界在哪里,全得自己拼。后来我花了很长时间把Camera Service、Provider、HAL三层串起来,才算真正看懂这套设计的精妙。这篇博文就是当时整理的架构笔记,适合做Camera应用接入、HAL实现、或者多媒体框架集成的开发者看,目标是把HAL3这个抽象的"硬件抽象层"讲成一条能跑通的链路。
文章不会把camera3.h里每个tag都过一遍,那属于查文档的事。我会把重点放在架构设计上:为什么需要Request-Result模型、四类核心数据结构怎么协作、一次拍照在HAL内部经历了什么、以及我在这套接口上踩过的几个典型坑。你把这篇文章当成一份"带路笔记"来看,遇到具体问题时再回去翻头文件和AOSP源码,会顺畅很多。
1. 从HAL1到HAL3:系统要解决的根本矛盾
1.1 HAL1的简单模型与它的天花板
HAL1时代,摄像头硬件抽象层的设计思路非常简单:给上层暴露一个单一设备接口,Preview、拍照、录像、参数控制全都通过较大粒度的命令走。这个设计在功能比较单一的时代不算什么问题,但到多摄、高帧率、RAW、HDR、连拍需求越来越多的时候,短板非常明显。应用层拿到的是裁剪后的能力,一些原本可以通过ISP流水线并行处理的场景(边预览边拍照、边录像边取帧)被强行串行化;多路数据流之间的同步没有标准模型,vendor各自实现导致兼容性问题。整个系统是"一层包一层",但每一层都缺少对底层能力的完整抽象。
HAL1的真正天花板在于它的控制模型是"命令式"的:上层发多少条命令、命令粒度多细,完全取决于HAL1接口愿意暴露多少方法。命令式抽象做不到对数量庞大且可组合的控制参数进行逐帧管理,也解决不了Camera Service和Vendor层之间"一次配置、持续流水线"的需求。当时越来越多的厂商开始尝试在多摄融合、AI场景识别这类功能上做文章,靠HAL1的接口很难在不破坏上层框架的情况下支持。
大概就是这样一个背景,HAL3的Request-Result模型在Android框架里面被确定下来。它没有把控制指令做成一个个命令函数,而是把"一次拍摄/一帧采集"定义为一个结构化对象——CaptureRequest,把所有设备状态、3A、传感器曝光等参数以Metadata的形式放进去;再把每一次出帧定义为对应CaptureResult。上层只需要不断提交Request,底层负责把它翻译成硬件流水线上的具体动作。命令式变成数据驱动,能力边界一下子打开。
这套模型带来的直接收益是:上层可以做非常细粒度的帧级控制,也可以把多路session并行提交;vendor层则可以自由选择用软件、硬件、混合流水线来处理请求。HAL1到HAL3的迁移,本质上是把"摄像头当作一个可以任意编程的流水线设备"来重新做接口设计。
1.2 为什么Request-Result能解决HAL1的问题
先看一个具体例子:连拍。HAL1的连拍逻辑倾向于依靠命令循环,底层设备进入"连拍模式",上层接收一连串JPEG;想在同一场景里既做预览又做拍照,就得在命令级做切换。HAL3里,连拍就是一个Request序列,每一帧都携带完整的AE/AF/AWB参数,甚至可以单独调整每一帧的曝光时间、ISO、裁剪区域。由于Request本身是完整且可组合的,上层可以做"连续帧相同参数连拍",也可以做"连续帧逐帧渐变参数连拍",Camera Service不需要为不同连拍策略创建不同的hal命令。
而"边预览边拍照"就变成了一个简单的流的组合:预览流和JPEG流同时配置在stream_configuration中,同一个Request的output_buffers里同时挂两条流对应的buffer。管线能不能并行,是HAL内部根据流配置自己决定的事情。这种模型还有一个天然优势:可测试性和可移植性。Request、Result、Metadata、Stream都是结构化数据,Framework层可以做更加统一的验证,vendor层也可以把同一套接口运行在不同硬件上。
对做系统集成的同学而言,最大的感觉是接口的"契约感"变强了:HAL不再通过一堆命令解释器接收语义,而是通过明确的struct和callback完成一次数据闭环。理解了这条闭环,就能理解HAL3内部大部分文件——它们都是在为这条闭环服务。这也是我在最初看代码时一直提醒自己的:看到camera3_device_t里的函数指针,不要把它们当作孤立API,要当作流程上的站点。接下来我就按这条闭环,把每一站拆开讲。
2. 整体架构分层:Camera Service、Provider与HAL的职责边界
2.1 Camera Service侧与控制链
先从整体系统视角看。Android的Camera子系统从使用者角度看大致三层。最上面是应用使用的Camera2 API / CameraX,往下是Camera Service,再往下是Camera HAL3抽象。在Treble架构之后,Camera HAL被抽到了独立的vendor进程,system侧的Camera Service通过CameraProvider接口来访问vendor侧的CameraDevice实现。这个拆分不只是为了避接口,更是为了隔离:vendor的崩溃不能把system进程拉下水,system的更新也不要求vendor重编。
控制链路上,应用通过CameraDevice.createCaptureSession创建Session并提交CaptureRequest,Camera Service收到请求后会进行权限校验、Stream配置校验、Metadata空间分配,然后组装成HAL层能识别的camera3_capture_request_t。这里有一个值得注意的点:CameraService本身就会对Request的合法性做一遍预校验,比如检查output_buffers里引用的stream是否在configure_streams时配置过、settings是否是有效的camera_metadata_t、buffer的fence是否处于可用状态。这意味着HAL实现里不应当假设每次process_capture_request收到的参数都是靠谱的,按契约实现接口即可,不需要过度防御。
我在调试早期HAL实现时,浪费了不少时间在"帮上层兜底"上面,后来发现兜底反而掩盖了上游问题,尽早让非法请求失败才对排障有利。HAL层需要实现的接口全部集中在camera3.h中,核心是camera3_device_t和camera3_device_ops_t。open底层视频节点这类操作在HAL内部完成,camera3_device_t是HAL暴露给上层的一个抽象设备实例,它的ops指针就是整个HAL3入口。
实现时需要提供initialize、configure_streams、construct_default_request_settings、process_capture_request、dump、flush这几个最基本的函数。initialize在上层拿到设备后首先调用,用来回调注册;configure_streams负责建立流拓扑;construct_default_request_settings用来快速生成带默认3A参数的Request模板;process_capture_request是主工作流入口;flush用于强制取消后续requests。这些接口各自的时序和约束我会在第4章里展开,先记住它们在同一个ops表中就够了。
2.2 HAL侧的三个核心数据结构
把递进式的关系写出来:一个camera3_device_t对应一个物理/逻辑摄像头;一个设备可以同时配置多个Stream,Stream组成流配置(camera3_stream_configuration_t);而每一帧的工作则是从一个CaptureRequest开始,到CaptureResult结束。HAL实现者大部分时间打交道的,就是这三类数据的生命期管理。
| 结构体 | 职责 | 生命周期要点 |
|---|---|---|
| camera3_device_t | 抽象设备实例 | open时分配,close时释放 |
| camera3_stream_t | 一条buffer流 | configure时创建,重配置后失效 |
| camera3_capture_request_t | 单帧请求 | process返回后即可销毁 |
| camera3_capture_result_t | 单帧结果 | 回调返回后不可复用 |
先看Stream。camera3_stream_t描述一条buffer流:宽度、高度、PixelFormat、usage flags、stream_type(OUTPUT/INPUT/BIDIRECTIONAL)、rotation、max_buffers。它是在configure_streams时由CameraService创建好、HAL填写的。有一个容易搞混的点:max_buffers不是CameraService决定的,而是HAL在configure_streams阶段填写的,表示HAL这张流上最多需要同时驻留多少个buffer。填太大浪费,填太小会导致上层来不及排队,最终表现为预览掉帧或录像帧率上不去。
再看Request。camera3_capture_request_t描述了某一帧的所有输入:frame_number是单调递增的编号,贯穿一整条流水线;settings是camera_metadata_t,包含该帧对3A/曝光/焦距等控制参数的定义;output_buffers是camera3_stream_buffer_t数组,每个元素把一个具体buffer handle绑定到一条stream上,并携带acquire_fence/release_fence。如果涉及ZSL或输入流,input_buffer用于指定输入buffer。
很多同学第一次看camera3_stream_buffer_t时,会被其中那两根fence绕晕。我的理解是:acquire_fence是HAL必须等待的"上游信号",只有等它变为signaled,HAL才能读取或写入这个buffer;release_fence是HAL会在处理好buffer后写入的信号,告诉下游"现在可以安全消费了"。fence的本质就是一个带文件描述符的同步对象,Linux下可以通过sync系列接口等待和合并,处理不当会卡住整条流水线。这部分非常重要,我在第5章专门用一个小节讲真实踩坑。
Result的形态和Request对称。camera3_capture_result_t包含同一frame_number的result metadata、output buffer状态、以及每个物理相机的metadata(多摄时代)。除了process_capture_result,HAL还要通过camera3_callback_ops_t的notify回调上报异步事件:最常见的有CAMERA3_MSG_SHUTTER(曝光开始时刻的硬件时间戳,对录像/预览同步至关重要)和CAMERA3_MSG_ERROR(设备错误、请求错误、缓冲区错误等)。调用的时序要求是:对于同一frame_number,HAL必须在调用process_capture_result之前或同时调用notify(ERROR_BUFFER)等错误通知,shutter事件则要在result之前上报。
2.3 回调路径与线程模型
HAL3的线程模型在设计上是自由的:框架并不规定HAL内部有几个线程,只对行为契约有要求。但实际实现最好遵循几个温和的约定。第一,不要把长耗时操作放在回调线程里,因为在process_capture_result或notify执行的代码会阻断上层流水线。第二,来自不同上层调用的并发是真实存在的:configure_streams在运行的时候可能同时有process_capture_request在排队,flush和process_capture_request之间也会竞争。HAL内部要有一个清晰的请求队列,以及对应的worker线程,避免直接在process_capture_request里同步做完整帧处理。
第三,HAL不要把settings指针保留到下一次process调用之后,一般需要深拷贝metadata,除非你非常确定上游不会修改它。曾经有过HAL只保存指针、上一个请求还没处理完就被覆盖,导致部分帧用了错误曝光参数的案例。
我在最初实现自己的HAL demo时,犯过的错误是把process_capture_request直接当成"立即处理"入口,所有硬件操作都在这个线程里串行执行,短时间卡住还能将就,一旦AE/AF等待时间上浮到几十毫秒,整个request队列就全部堵塞,连预览都开始断流。后来改成标准的"请求入队+worker消费+结果异步回传"模型,稳定性一下就上来了。这也是HAL3和HAL1差别很大的点:上层对你的异步性是有预期的,你的实现结构如果把自己的并发能力做成串行,等于替整个系统引入了瓶颈。
3. 数据流核心:Stream配置、Request语义与Buffer生命周期
3.1 从configure_streams看流拓扑的建立
应用每次调用createCaptureSession的时候,最终都会走到HAL的configure_streams。这个调用会让HAL重新评估当前设备所面向的输出面。预览流在Framework侧往往关联到SurfaceView/TextureView的Surface;录像流关联到MediaCodec的input surface;拍照流则通常是一个ImageReader对应的Producer。HAL在configure_streams里拿到的是抽象描述,不是已经与具体Surface绑定的buffer。
这非常重要:HAL不需要知道buffer最终被谁消费,只需要根据Stream的width/height/format/usage规划内部的ISP路径、分配内存池、决定max_buffers以及内部旋转策略。configure_streams一个关键点是:它不是只能调用一次的。Framework允许应用在会话运行过程中动态更新流配置,例如打开相机后先配置两路预览加一路拍照,之后为了更高帧率把其中一路降配。
HAL需要支持在运行中被再次调用configure_streams,并且应当把之前所有pending的request通过flush清理干净,再进入新配置。这里vendor实现特别容易出一个问题:旧stream上还有buffer没释放就把stream pointer替换掉了,流程走下去就会在process_capture_result里引用一个不再存在的stream。我的做法是:在收到新的configure_streams后,先把旧stream的所有pending buffer归还到合法状态,再进入新配置;在HAL内部维护一张stream_id到stream状态表的映射,所有处于working状态的request都在这张表里登记过,才能确保替换安全。
constrain_high_speed模式是另一个容易忽略的分支。operation_mode设置为CAMERA3_STREAM_CONFIGURATION_CONSTRAINED_HIGH_SPEED_MODE时,Framework侧要求HAL实现一个受限的高速连拍/多路同步模式,典型场景是双摄高速预览。在这个模式下,stream的数量、buffer数量、帧时间控制都可能受到额外约束,HAL如果不去读operation_mode就直接复用普通模式的配置,上层给的HighSpeedSession往往会报错。实现高速模式下,除了流数量,还需要特别注意每张stream的max_buffers不能过大,否则内存会超出预期;反过来太小又会限制连拍深度。这个值通常需要根据ISP流水线深度、内存带宽来压测校准。
3.2 CaptureRequest里的"一帧决策"
单个CaptureRequest是最小控制单元,但它承载的语义非常丰富。settings里可以携带ANDROID_CONTROL_CAPTURE_INTENT(预览/拍照/录像/高速等)、ANDROID_SENSOR_EXPOSURE_TIME、ANDROID_SENSOR_SENSITIVITY、ANDROID_CONTROL_AE_REGIONS、ANDROID_LENS_FOCUS_DISTANCE等tag;也可以用ANDROID_CONTROL_ENABLE_ZSL等标签控制特殊模式。
处理一个Request时,HAL先根据settings构造一帧的硬件控制参数,再执行传感器/ISP的调度。很多vendor实现里,它并不会针对每个Request都改一整套寄存器,而是会缓存上一帧的settings,只在有变化时触发3A overlay更新;这样做的理由是效率,纯粹逐帧重算3A会白白增加几十毫秒的空窗。
output_buffers的定义也值得展开。同一帧可以从同一Request派生出多条流:例如JPEG流和YUV预览流同时出现在output_buffers里,HAL需要保证它们在硬件流水中是同步产生的,不能一个快一个慢。这一点对于那种"先拍JPEG再转YUV"的软处理方式尤其要小心:如果JPEG缓冲在ISP的JPEG编码器里和预览缓冲在另一个处理单元里完成时间差很大,process_capture_result虽然可以分别回传,但上层通常会根据metadata里的同步信息做对齐。一个工程上的妥协是,如果实现做不了完全同步,至少保证每条buffer的release_fence时间点和对应的metadata时间戳是真实一致的,这可以避免很多上层错帧/掉帧问题。
读settings还有一个容易踩的坑:CameraService传进来的metadata不一定都是完全合法的。某些tag组合在逻辑上就是互相冲突的,比如同时指定手动曝光又开启AE。HAL通常不需要自己写一套完整校验器,直接信任标准组合即可,但要对明显破坏性的值做保护,比如零尺寸、负曝光、无效buffer handle。早期实现阶段建议加一些防御性assert,这样vendor调试时能快速暴露Framework侧的错误,否则问题会一路漂移到应用层表现为黑屏或崩溃。
3.3 Fence:两个文件描述符撑起的同步边界
为什么HAL3把buffer的同步完全交给fence而不是靠锁?因为参与buffer生产的模块可能是ISP、GPU、DMA、硬件编解码器,各自独立运行,用锁等待会让整个流水线的并发基本上退化成串行;fence则允许不同硬件在时间上重叠,谁准备好谁就继续,真正做到流水线并行。
acquire_fence一般是上游(比如应用的ImageWriter、传感器输入的DMA)写入的,表示写者已经完成,读者可以开始。release_fence是HAL在完成本阶段读写后写下的"完成信号",下游(比如摄像机框架把buffer交给VideoEncoder或合成器)必须等待release_fence信号才能读取。落实到代码上,HAL在处理output buffer时要做两件事:先等待acquire_fence到达signaled,再在写入完成后把release_fence传给框架。
很多早期HAL实现图省事直接把两个fence都当作-1处理,看起来跑得通,但实际上把同步职责全部承担到CPU侧,或者干脆放弃了栅栏,在真实设备上会不定期出现画面撕裂、帧顺序错乱、编解码器花屏。fence操作本身要用对方法。大多数实现用sync_wait(fd, timeout)等待,用sync_merge产生一组合并后的fence,再用dup/close管理文件描述符生命周期。
提示:fence的fd所有权要非常明确——谁signaled谁负责;交出时要dup一份,自己持有的那份完成后close,不能图省事把原始fd直接塞进result里。
一个经典问题是:HAL把同一个fd同时用于操作用后又close掉,而上层还没有signal,就会在framework层出现use-after-close的诡异错误,表现为帧虽然返回但内容永远不对。另外不要在acquire_fence上做无限期等待,建议给一个可配置的timeout(比如3000ms),超时后按CAMERA3_BUFFER_STATUS_ERROR返回,避免硬件异常把整条流水线钉死。所有看起来"偶发"卡帧,一半以上最终都能在fence使用方式上找到原因。
4. 从第一次configure到持续出帧:一次流水的完整链路
4.1 一个标准的设备打开与配置序列
给一个时序:应用open camera并createCaptureSession,CameraService创建camera3_device_t并调用initialize,传入camera3_callback_ops_t;随后Framework发起configure_streams,HAL返回各stream的max_buffers;之后construct_default_request_settings会被用于生成默认模板,应用一拿到Session即可提交第一个Request。
打开设备这一层虽然没有特别复杂,但initialize的时序经常被忽略:initialize必须在任何configure_streams或process_capture_request之前完成,还要在initialize里注册一个"对当前vendor所有设备状态变化"的回调路径。callback_ops是整个HAL向上层的唯一通道,后续所有的process_capture_result与notify都是通过它出去的。
一个很小的实现注意点:很多vendor会把自己私有参数挂在vendor tag序列里,如果initialize的时候没有把get_metadata_vendor_tag_ops配置好,上层使用私有tag时会在metadata库里直接报"unknown tag",排查起来很容易走弯路。建议在写完最小设备实现的同时,就把vendor tag的注册和dump逻辑一并做了,别等真要用时再回头补。
4.2 process_capture_request:HAL内部的主循环
process_capture_request是工作量最大、也是最容易出bug的地方。一次典型实现要经历这么几步:校验request指针和frame_number单调性;查找或创建一个该frame对应的内部job对象,绑定output buffers;将settings解析成硬件控制块(曝光/增益/焦距等);判断是否需要等待acquire_fence,如果不需要等就立即交给ISP/DMA队列;在worker线程上消耗硬件资源完成输出;填充result metadata;设置release_fence后,通过callback_ops回传camera3_capture_result_t。
这一步里最值得强调的是"尽早回传"策略。因为HAL的result既能一次性完整回传,也允许分多次partial result回传。Framework的设计允许多个partial批次,应用层可以通过CaptureCallback的partialResultCount参数拿到中间状态。对3A类结果尤其合适:AE/AF已经在帧开始时就确定了,可以把这些metadata在曝光结束、JPEG还没编码完成前先回传过去,应用就能提前更新曝光状态UI;而JPEG buffer可以等编码完成后单独回传。
很多HAL实现为了代码简单,把所有内容攒到最后一并返回,结果就是"帧率很漂亮,但应用UI和快门响应总是慢半拍"。如果能用partial机制把3A状态提前释放出去,用户体验能提升一大截。
还有一个容易被忽略的细节:request里output buffer的顺序并不保证和上一次调用一致,HAL不能缓存上一帧的buffer顺序作为下一帧的假设。buffer handle的ownership也是一次性的,不要跨帧持有。如果确实需要多帧合成/超分辨率,必须用input stream + ZSL的机制,而不能自己偷偷保留output buffer不做release。框架允许HAL在ZSL场景里复用内部buffer,但对output buffer的任何长期持有都会违反BufferQueue的约定,最终会变成分配器内存泄漏或使用被释放内存。
4.3 Result回传和Error上报的时机
process_capture_result里回调的内容有一个硬性时间约定:shutter事件必须发生在process_capture_result之前。时间戳用哪个时钟也值得注意:Android框架要求shutter timestamp是CLOCK_MONOTONIC域(未经过系统时间调整),很多vendor实现里直接用硬件读到的时间戳但忘了把域转到monotonic,播放录像文件时就会看到时间轴跳动或A/V不同步。如果对域的转换没把握,宁可再wrap一层转换为monotonic,也不能带着原始硬件拍到的"开机以来微秒"直接上报。
Error事件的处理按类型分开说:CAMERA3_MSG_ERROR_REQUEST表示单个request失败了,后续result不应该再为该frame回传buffer;CAMERA3_MSG_ERROR_BUFFER表示某个buffer生成失败,需要把该buffer的status置为CAMERA3_BUFFER_STATUS_ERROR,并且fence还是传正常fd可能导致上层等超时。我的做法是:buffer失败时直接给status=ERROR,acquire/release都设为-1,这样Framework能立即识别错误并把该帧标记失败,不会等fence超时。
CAMERA3_MSG_ERROR_DEVICE表示整个设备层面的问题,上报后框架会拉断Session。这三个错误类型在排障时看logcat非常有用,HAL侧尽量把"哪一帧、哪个stream、什么原因"日志打全,别只返回一个类型码。
每次process_capture_request被调用,都可能立即触发下一个request,所以不要在该函数里做大量同步等待。HAL真正应该做的是:把request翻译成内部任务,放到队列,立刻返回。队列深度决定了可并行帧数;队列越深、能够覆盖隐藏延迟,但也会增加第一帧到输出结果的latency。这个tradeoff要靠实际设备效果校准,一般preview场景维护2~3帧的深度比较常见,连拍场景可以更浅。我一开始把队列长度拍脑袋设成16,结果就是拍照的机械快门延迟肉眼可见;缩回到3之后延迟和流畅度都到了一个可用区间。
5. 多年踩坑经验:configure、Buffer与旋转这些事
5.1 max_buffers与BufferQueue:为什么预览会莫名掉帧
max_buffers的填法非常微妙。Framework侧对每条stream会创建一个BufferQueue,HAL填的max_buffers会作为queue可分配buffer的一个上限参考。如果你把max_buffers填成1,那预览流上基本只能有1帧在飞,一旦硬件还没处理完、上层又要投递下一帧,系统就无buffer可用,表现为掉帧/卡顿/黑屏闪烁。反过来填成几十,设备内存会被无谓占用,大型连拍和高分辨率流的场景下,OOM风险直线上升。
合理的max_buffers应当约等于"你HAL内部所有同时需要的buffer数量":包括硬件工作队列里未完成的、正在等待fence的、以及你为了流水线连续而预留的。
有个非常容易误踩的场景:你在configure_streams里返回的max_buffers比较小,但上层实际会因为拍照/录像提高buffer需求,框架侧的BufferQueue会自动尝试再向gralloc分配额外的buffer;而gralloc分配出来的buffer格式、usage如果和stream的配置不一致,就会被Camera Service驳回,上层看到的错误五花八门:有时候是configureStreamsInternalBufferAllocationFailed,有时候是预览黑屏。所以在返回max_buffers时,不仅考虑数量,还要确保和本机gralloc分配策略兼容,不同硬件平台上这个数字往往差异很大,不能只依赖参考实现照抄。
5.2 rotation:传感器拍摄方向与预览方向的拉扯
这块是HAL3里最容易被忽略的架构设计点。传感器芯片是固定安装在设备里的,通常有一个sensor orientation(比如0/90/180/270),表示原始传感器输出图像相对于设备自然方向需要旋转多少度才能显示正确。框架不会替HAL完成这件事,而是通过camera3_stream_t的rotation字段告诉HAL:这条stream的消费者希望拿到已经旋转到指定方向的buffer。
如果你的HAL里没有做旋转处理,而只是沿用ISP原始输出,预览画面就会出现90度或者说镜像,在部分应用上还能用简单的TextureView矩阵转回来,但Recorder、视频流和拍照的JPEG流就没办法了,因为它们的exif/帧宽高在框架侧已经按stream配置锁定。
提示:旋转处理要尽早做到流配置层,不要等到process时做。一种常见做法是在configure_streams阶段根据sensor orientation和stream.rotation计算出ISP是否要在scaler模块里做裁剪旋转,并把相应的scaler输出大小填回stream的width/height。
这里有个坑:HAL要小心不要同时把rotation又通过JPEG orientation metadata再旋转一次,那会得到二次旋转的图。正确的分工是:流上交给consumer的是物理旋转后的图像,而metadata里的ANDROID_JPEG_ORIENTATION保留原始方向信息,供上层做最终展示判断。两套方向机制叠加使用,才能保证预览正、拍照图也正。
5.3 flush和并发调用:最容易把系统"弄哑"的地方
flush在HAL3中的定位是"尽快取消所有pending requests并返回"。框架在某些场景下(比如configure动态切换、会话关闭、设备出错恢复)会调用flush。很多HAL实现会在flush里直接清空请求队列、打断正在运行的worker线程,但忘记回收pending输出buffer的引用,这会导致这些buffer的lease永远卡在上层手上,最终整个BufferQueue被饿死。
我在code review时最坚持的原则是:flush完成时,所有尚未返回的buffer必须通过process_capture_result(带ERROR/BUFFER_STATUS)或notify(ERROR_REQUEST)显式归还,不允许默默丢弃。这一条写进HAL实现规范里后,很多"偶发无法重连相机"的问题就消失了。
另一个并发点是process_capture_request和flush/configure_streams不会天然串行。框架文档允许这些调用发生在不同线程上。HAL里如果只靠一把全局锁,性能会非常拉胯;如果完全不加锁,又会在flush执行中收到新的request,导致状态错乱。我的做法是设计一个"状态机+消息队列":request永远先push进队列,worker线程从队列消费;flush只是向队列投放一个barrier任务,worker处理到barrier时统一取消排在其后的所有任务并返回。这种做法既保证并发安全,又避免像"多把锁嵌套"那种很难复现的死锁。
6. 最小HAL3骨架与调试手段:从能跑到跑得稳
6.1 一个极简HAL的模块结构
与其空谈架构,不如给一个能让系统首次跑通的骨架。这里的代码我刻意省略了具体的硬件操作,只保留框架必须的接口形状,理解之后可以套到自己的硬件上:
static camera3_device_ops_t s_ops = { .initialize = dev_initialize, .configure_streams = dev_configure_streams, .construct_default_request_settings = dev_default_settings, .process_capture_request = dev_process_request, .flush = dev_flush, .dump = dev_dump, }; static int dev_open(const hw_module_t *module, const char *name, hw_device_t **device) { // 1. 分配并初始化camera3_device_t // 2. 挂上&s_ops // 3. 按照name查找到对应camera id // 4. 初始化内部请求队列/worker线程 }initialize实现里只需要保存camera3_callback_ops_t指针并做一些设备能力查询;configure_streams实现里遍历streams数组,根据format和size填充HAL侧的max_buffers与内部流水线配置。process_capture_request则是把request对象深拷贝到队列,worker线程里再逐字段解析并最终通过ops->process_capture_result和ops->notify回传。
这个骨架最小到什么程度?它甚至不需要真正访问sensor硬件,可以用一个模拟数据源在帧率达到30fps时往output buffer里填一张纯色图。我说这个例子的目的,是想说HAL3的架构本身是高度可测试的。你可以先在没有硬件的情况下,把接口跑通、把fence生命周期调对,再逐步把真实ISP/传感器接进来。这样最后联调的问题会集中在硬件控制本身,而不是接口理解错误。
6.2 用日志与工具建立调试链路
HAL侧的调试手段主要围绕两条线:一是Android Framework的dumpsys、logcat,二是vendor侧自己加的调试接口。使用adb shell dumpsys media.camera可以dump出所有CameraDeviceClient的session状态、stream配置、buffer状态以及最近几次process_capture_result的统计,查看哪里有pending frame、哪条stream长时间没有新buffer,定位问题节点很直接。
logcat中cameraserver进程的日志会输出framework侧对HAL调用的返回值、错误类型。如果返回值是负的错误码,第一件事先对接口文档,别先猜硬件问题;次数多了你会发现框架侧的日志基本能定位到99%的参数问题。vendor侧建议至少增加两类自定义调试手段:一类是"内部状态快照",调试时通过一个私有tag或sysfs导出HAL内部的request队列深度、buffer分配数量、fence等待时间;另一类是"压力测试",写一个循环脚本持续做"配置会话-提交请求-释放会话"动作,测试事件序列覆盖不到的内存泄漏和资源回收。
我在调试一个HAL时,最受益的其实是给每一个"分配到buffer handle但还没归还"的对象做引用计数日志,发现leak的过程比猜要快得多。
6.3 性能优化顺序:先看同步再看频率
最后聊两句性能。做HAL3性能优化,我的顺序是:第一优先级是fence同步,因为任何一个异常等待都会直接拖垮整条流水线,而且这类问题通常表现为"偶发卡一下",很迷惑人;第二优先级是request处理路径上的内存分配,每帧都malloc metadata、再逐项copy,在高帧率下会明显增加CPU stall,最好用对象池或者复用已有metadata;第三优先级才是硬件寄存器调度、ISP路径之类的细调。
如果帧率上不去,先用dumpsys查是不是有request长期滞留在队列里,有就查fence,没有再看metadata分配。曾有一个case,预览画面偶发跳帧且没有规律。刚开始怀疑ISP超时,花了很久调参没有效果。后来在HAL里给acquire_fence的等待加了时间戳统计,发现每几秒都会有一个buffer的fence等待超过几十毫秒,一查是gralloc分配的buffer跨了内存区,多路共享了同一块物理内存,DMA写同一块区域造成竞争。改成分区内不同的物理内存后问题直接消失。
这个经验的教训是:HAL3的架构问题往往最后表现成一个很表面的"帧率问题",但根因可能在底层buffer分配策略,不要把搜索范围局限在HAL自己的代码里。先沿着fence把同步链路捋清楚,再去看buffer分配和metadata复制,大部分疑难杂症都能按这个顺序顺藤摸瓜找到根因。