简介:这份资源面向Android车机与车载影像方向的开发者,聚焦高通平台上的EVS(Exterior View System)实现,涵盖EVS1.0与EVS2.0在硬件抽象层(HAL)上的差异与适配思路,可用于后视、环视、行车记录及ADAS等车载相机场景的学习与二次开发。压缩包共124个文件,约366KB,以46个h头文件与43个cpp源文件为主体,配合8个mk编译脚本、5个rc启动配置及若干xml、json、license等辅助文件,结构上覆盖相机枚举、状态控制、配置管理与OpenGL渲染等模块,便于直接编译并调整开机启动方式。目前已有1651人学习下载。读者可借此理解EVS在Android车机中的HAL分层设计、编译与启动配置方法,并参考图像处理与多相机管理思路,为车载影像功能开发与调试提供可复用的工程基础。
1. 车机影像的"最后一公里":为什么 EVS 值得单独拆开看
倒车影像从挂上倒挡到画面出现,中间到底发生了什么?很多做 Android 应用开发的人第一反应是"调个 Camera API 不就行了",但真正在车机上做过影像系统的人会告诉你,从摄像头传感器出图到屏幕点亮,中间隔着一条比手机 Camera 复杂得多的链路。EVS(Exterior View System)就是这条链路上最核心的一环,它不是简单的"打开摄像头预览",而是一套从 HAL 层到应用层、从 buffer 管理到显示合成的完整子系统。
搞懂 EVS 的价值在于:它是车机 Android 里少数几个必须绕过标准 Camera 框架、直接对接底层硬件的场景。为什么?因为倒车影像有硬性时间要求——挂倒挡到出图通常要求在 2 秒以内,部分车厂甚至压到 1.5 秒。标准 Android Camera 框架从应用启动、权限检查、CameraService 绑定到预览流建立,冷启动动辄三四秒,根本来不及。EVS 的设计动机就是绕开这套重量级流程,用一条更短、更可控的通路把画面送到屏幕上。
2. EVS 与标准 Camera 框架:为什么不能复用同一条路
2.1 标准 Camera 框架的启动开销到底花在哪
要理解 EVS 为什么存在,得先看清标准 Camera 框架在车机场景下"慢"在哪。Android 的 Camera2/CameraX 链路大致是这样的:应用进程通过 Binder 调用 CameraService,CameraService 再通过 HIDL/AIDL 与 Camera Provider 通信,Provider 加载对应的 HAL 实现,HAL 打开硬件设备、配置流、分配 buffer,最后通过 Surface 把图像送回应用。这条链路每一层都有进程间通信、权限校验和状态机切换。
在手机上这套设计是合理的,因为 Camera 要服务几十种场景——拍照、录像、多摄切换、第三方 App 调用,必须有一套统一的资源仲裁机制。但车机的倒车影像场景极其单一:固定摄像头、固定分辨率、固定显示区域,不需要仲裁,不需要动态切换,唯一的要求就是快。用一套为通用性设计的框架去跑一个专用场景,就像用货运列车调度系统去送一份外卖,流程没错,但每一站都是开销。
常见的实现思路是:EVS 在 HAL 层直接打开摄像头设备节点(通常是 V4L2 的/dev/videoX),拿到图像后通过 Gralloc 分配的 graphic buffer 直接送到 SurfaceFlinger 合成显示,整条路径不经过 CameraService,也不经过应用层的 Camera API。这就是它快的根本原因——不是优化了某一步,而是砍掉了整条中间链路。
2.2 EVS 的进程模型与数据通路
EVS 在 Android 里的进程模型值得单独说清楚。它通常以一个独立的 native 服务进程存在,源码结构上对应packages/services/Car/cpp/evs目录下的一组组件。核心分三块:EVS HAL(硬件抽象层,负责和摄像头驱动打交道)、EVS Manager(管理多个摄像头的枚举和状态)、EVS App(负责把图像送到显示层)。
数据通路是这样的:摄像头驱动出图 → V4L2 接口读取 → EVS HAL 封装成EvsCamera接口 → 通过IEvsCamera的 HIDL/AIDL 接口把 buffer 传给 EVS App → EVS App 用EGL或直接Surface送显。这里有个关键设计:buffer 是预分配、循环复用的,不是每帧新申请。EVS HAL 在初始化阶段就会分配一组固定数量的 graphic buffer(通常是 4 到 8 个),形成一个 buffer 池,摄像头往里写,显示端从里读,用完归还。这避免了每帧内存分配带来的延迟抖动。
// EVS HAL 中 buffer 池的典型初始化逻辑(简化示意) // 关键点:buffer 数量固定,格式固定,一次性分配后循环使用 struct EvsBufferPool { std::vector<BufferDesc> buffers; // 预分配的 buffer 描述符 std::queue<uint32_t> freeList; // 空闲 buffer 索引队列 bool init(uint32_t count, uint32_t width, uint32_t height, int format) { for (uint32_t i = 0; i < count; i++) { BufferDesc desc; // 通过 Gralloc 分配 graphic buffer,格式通常是 YUV420 或 RGB desc.handle = allocGraphicBuffer(width, height, format); desc.stride = getStride(desc.handle); // 硬件对齐后的行宽 buffers.push_back(desc); freeList.push(i); // 初始全部空闲 } return true; } // 摄像头出图时调用:从空闲队列取一个 buffer 交给硬件填充 int acquireBuffer() { if (freeList.empty()) return -1; // 池耗尽,说明消费端太慢 int idx = freeList.front(); freeList.pop(); return idx; } // 显示端消费完后调用:归还 buffer,形成循环 void releaseBuffer(int idx) { freeList.push(idx); } };这段代码里最值得注意的是stride这个概念。摄像头输出的图像宽度和实际内存中每行的字节数往往不一样,硬件要求行首地址按一定字节对齐(比如 16 或 64 字节对齐),所以stride通常大于width。如果显示端按width去读内存,画面就会出现斜纹或错位——这是 EVS 调试中最常见的翻车点之一。buffer 池的大小选择也是个取舍:太小容易在消费端卡顿时耗尽,太大则增加内存占用和首帧延迟,实践中 4 到 6 个是比较常见的平衡点。
3. 从 HAL 到显示:EVS 的接口设计与状态机
3.1 IEvsCamera 接口的核心方法
EVS HAL 对外暴露的核心接口是IEvsCamera,它定义了几个关键方法:getCameraInfo返回摄像头的基本信息(分辨率、格式、方向),setMaxFramesInFlight设置同时在途的最大帧数,startVideoStream启动视频流并注册回调,doneWithFrame通知 HAL 某一帧已经消费完毕可以回收。
这套接口的设计思路是生产者-消费者模型的最小化表达。HAL 是生产者,通过回调把填充好的 buffer 推给消费者;消费者处理完后必须显式调用doneWithFrame归还 buffer,否则 buffer 池很快耗尽,视频流就会卡住。这个"显式归还"机制是 EVS 稳定性的关键——它把 buffer 生命周期的责任交给了消费端,HAL 不主动回收,避免了"显示端还在读、HAL 已经覆写"的竞态。
// EVS App 侧接收帧并送显的典型流程(简化示意) class EvsDisplayConsumer : public IEvsCameraStream { // HAL 每出一帧就回调这里 Return<void> deliverFrame(const BufferDesc& buffer) override { // 1. 把 buffer 送到显示 Surface // 这里可能涉及 EGL 纹理绑定或直接 Surface 拷贝 renderToDisplay(buffer); // 2. 关键:消费完必须归还,否则 buffer 池会耗尽 // 注意这里传的是 bufferId,不是 buffer 本身 camera->doneWithFrame(buffer.bufferId); return Void(); } };setMaxFramesInFlight这个参数容易被忽略,但它直接决定了系统的抗抖动能力。它告诉 HAL "最多允许多少帧同时在途",如果设得太小,摄像头出图稍快就会丢帧;设得太大,延迟会累积——因为队列里的帧越多,从采集到显示的时间差就越大。倒车影像场景下,延迟比流畅度更重要,所以这个值通常设得比较保守,2 到 3 帧是常见配置。
3.2 显示合成的路径选择
EVS 的图像送到屏幕有两条路:一条是走 SurfaceFlinger 合成,EVS App 创建一个 Surface,把图像写进去,由 SurfaceFlinger 统一合成后送显;另一条是绕过 SurfaceFlinger,直接用 DRM/KMS 接口把图层送到显示控制器。前者兼容性好、实现简单,但多了一层合成开销;后者延迟更低,但需要硬件和驱动支持,移植性差。
大多数量产方案走的是第一条路,因为车机的显示内容不只是倒车影像,还有仪表、导航、空调控制等,必须由 SurfaceFlinger 统一管理图层叠加和区域划分。EVS 的 Surface 通常被设置为最高优先级图层,确保倒车时能覆盖其他内容。这里的设计取舍是:牺牲一点延迟换取系统级的显示管理能力,而这点延迟通过 buffer 池的优化和图层优先级的设置可以压到可接受范围内。
4. 避坑与排查:EVS 调试中最容易踩的几个坑
4.1 画面花屏或斜纹
现象:倒车影像能出来,但画面有明显的斜向条纹或者左右错位。原因:几乎都是 stride 处理错误。摄像头输出的 buffer 每行有硬件对齐填充,如果显示端按图像实际宽度去读,就会把填充字节当成像素数据。解决:在送显时使用 buffer 描述符里的 stride 值而不是 width 值来计算行偏移,EGL 绑定纹理时也要用 stride 对应的参数设置GL_UNPACK_ROW_LENGTH。
4.2 挂倒挡后画面延迟明显
现象:画面能出,但从挂挡到看见图像要三四秒。原因:EVS 服务没有常驻,每次挂倒挡才启动,初始化摄像头、分配 buffer、建立显示连接全在关键路径上。解决:让 EVS HAL 和 Manager 常驻后台,摄像头设备保持打开状态但不出流,挂挡时只需启动视频流,省去初始化和分配的时间。这是量产方案的标准做法。
4.3 buffer 池耗尽导致画面卡死
现象:画面出来几秒后突然卡住不动,日志里能看到 acquireBuffer 返回失败。原因:消费端某条路径没有调用doneWithFrame,比如送显失败时直接 return 了,没有归还 buffer。解决:在所有异常分支上确保 buffer 归还,用 RAII 封装 buffer 的获取和释放,避免手动管理遗漏。同时可以在 HAL 侧加超时回收机制作为兜底。
4.4 多摄像头切换时黑屏
现象:单摄像头正常,切换到另一个摄像头时黑屏或报错。原因:EVS Manager 枚举摄像头时设备节点顺序不稳定,或者切换时旧摄像头的 buffer 没有完全释放就打开了新的。解决:不要依赖/dev/videoX的编号顺序,通过摄像头 ID 或设备属性来匹配;切换时先完整关闭旧流、归还所有 buffer,再启动新流。
4.5 低温环境下首帧出图慢
现象:常温正常,零下温度时倒车影像要五六秒才出来。原因:摄像头传感器在低温下需要更长的预热时间,自动曝光和白平衡收敛慢,前几帧图像质量差或直接丢弃。解决:在 HAL 层设置一个"预热帧丢弃"策略,前 N 帧不送显但也不阻塞流程,同时适当放宽首帧的超时阈值。这个坑在北方冬季的实车测试中非常常见。
5. 设计取舍:EVS 为什么不做成通用框架
回头看 EVS 的整套设计,它其实做了一个很明确的取舍:放弃通用性,换取确定性和低延迟。标准 Camera 框架为了支持无数种使用场景,引入了复杂的资源仲裁、动态配置和跨进程状态管理,这些在手机上是必要的,但在倒车影像这个单一场景下全是负担。EVS 选择了一条"专用通路"——固定格式、固定 buffer 池、固定显示路径,把变量降到最少,从而把延迟和不确定性压到最低。
这个思路在车机领域其实很普遍:仪表盘、HUD、倒车影像这些安全相关功能,往往都有自己独立的渲染通路,不走通用的 UI 框架。原因很简单——通用框架的每一次抽象都是一次不确定性来源,而安全相关功能最怕的就是"大部分时候正常,偶尔慢一下"。
我自己的习惯是,每次评估一个车机影像方案时,先不看它支持多少种摄像头、多少种分辨率,而是先问三个问题:buffer 池多大、在途帧数设多少、异常路径上 buffer 怎么归还。这三个问题答不清楚的方案,功能列表再漂亮我也不敢用。从那以后我每次做影像相关的评审,都会强制走一遍 buffer 生命周期的全路径检查,包括所有异常分支。希望帮到你。
本文还有配套的精品资源,点击获取