简介:这份资源面向Android车机与车载影像方向的开发者,聚焦高通平台上的EVS(Exterior View System)实现,覆盖EVS1.0与EVS2.0两代方案,帮助读者理解车载相机在HAL层的适配方式与启动控制逻辑。包内共124个文件,以46个h头文件、43个cpp源文件为核心,配合8个mk编译脚本、5个rc启动配置及xml、json等配置资源,压缩包约366KB,结构紧凑,便于直接编译验证。内容涉及EvsEnumerator、EvsStateControl、GlWrapper、RenderTopView等模块,可据此梳理环视、后视与行车记录等场景下的图像采集、渲染与状态切换流程,并掌握通过rc文件调整开机自启或分阶段启动的方法。目前已有1651人学习下载,适合具备一定Android系统与C++基础、希望深入车载影像HAL实现与ADAS相关功能的中高级开发者参考。
1. 车机影像系统落地:从倒车影像到多路环视的完整工程链路
很多做 Android 应用开发的同行第一次接触车机项目时,都会有一个错觉:不就是把手机上的 Camera 应用搬到车机上吗?我当初也是这么想的,直到真正拿到一份车载 EVS(Exterior View System)的源码包,才发现从应用层调用摄像头到画面稳定输出到中控屏,中间隔着 HAL 层、EVS Manager、V4L2 驱动、SurfaceFlinger 合成这一整条链路。这份资源围绕高通平台车机 Android 车载影像系统展开,核心解决的是倒车影像、环视拼接、多路摄像头切换在车机上的工程落地问题。它适合已经具备 Android 基础、想切入车载相机方向的开发者,也适合正在做车机系统定制、需要理解 EVS 服务架构的工程师。下面我按自己拆包的顺序,把这份资源从架构到代码到改造完整讲一遍。
2. 架构拆解:EVS 在 Android 车机里到底跑在哪一层
2.1 从应用到驱动:EVS 的纵向分层
车载影像系统和手机相机最大的区别在于:手机相机是「一个 App 对应一个 Camera」,而车机 EVS 是「系统级服务统一管理多路摄像头,按场景分发给不同应用」。这份资源里的工程结构清晰地体现了这个分层思路。
从上往下看,最上层是车机上的应用(倒车、环视、行车记录),它们不直接操作 Camera API,而是通过 CarService 提供的 EVS 接口获取视频流。中间层是 Android 的 EVS Manager 服务,它负责枚举摄像头、管理流会话、处理帧数据的分发。再往下是高通平台特有的 HAL 实现,通常以 HIDL 或 AIDL 接口暴露给上层。最底层是 V4L2 驱动,直接和摄像头硬件打交道。
这个分层带来的好处是:应用层不需要关心摄像头是 MIPI 还是 LVDS 接口,也不需要处理帧同步问题,只需要按 EVS Manager 定义的接口请求流就行。但代价是调试链路变长,一旦画面出不来,你得从应用层一路往下排查。
2.2 目录结构:拿到源码先看这几个目录
我拿到这份工程后,第一件事是看目录结构。车载 EVS 项目的目录组织通常遵循 Android 系统源码的惯例,但有几个关键目录需要重点关注:
# 典型的 EVS 工程目录结构 evs/ ├── app/ # 车机端应用层,倒车/环视 UI │ ├── src/main/java/ # 应用逻辑 │ └── src/main/res/ # 布局与资源 ├── hal/ # 高通平台 HAL 实现 │ ├── evs_hal.cpp # EVS HAL 入口 │ └── v4l2_camera.cpp # V4L2 摄像头操作封装 ├── manager/ # EVS Manager 服务 │ ├── EvsManager.cpp # 服务主逻辑 │ └── EvsStream.cpp # 流会话管理 ├── jni/ # JNI 桥接层 │ └── evs_jni.cpp # Java 与 Native 交互 └── config/ # 摄像头配置文件 └── evs_config.xml # 摄像头数量、分辨率、格式这个结构里,config/evs_config.xml是最容易被忽略但最关键的文件。它定义了系统启动时枚举哪些摄像头、每个摄像头的分辨率和对齐方式。如果配置文件写错,上层应用会直接报「找不到摄像头」,而不是给你一个模糊的错误。
2.3 数据流:一帧画面从摄像头到屏幕的旅程
理解数据流是调试 EVS 的基础。在这份工程里,一帧画面的完整路径是这样的:
摄像头硬件通过 MIPI CSI 接口输出 RAW 或 YUV 数据,V4L2 驱动把数据写入 DMA 缓冲区。EVS HAL 层通过VIDIOC_DQBUF从驱动取出缓冲区,然后根据配置决定是否做格式转换或缩放。接着,HAL 把帧数据通过 HIDL/AIDL 回调传给 EVS Manager。Manager 根据当前活跃的流会话,把帧分发给订阅了该摄像头的应用。应用层拿到帧后,通过 Surface 或 TextureView 渲染到屏幕。
这条链路里,任何一个环节的缓冲区管理出问题,都会导致画面卡顿、撕裂甚至黑屏。我后面会专门讲几个常见的翻车点。
3. 环境搭建与编译:把 EVS 工程跑起来的具体步骤
3.1 编译环境准备:依赖项一个都不能少
车载 EVS 工程对编译环境的要求比普通 Android 应用高得多。你需要一套完整的 Android 系统源码编译环境,而不是简单的 Android Studio 工程。常见做法是使用高通提供的 BSP 包,里面包含了平台相关的 HAL 和内核驱动。
基础依赖包括:Ubuntu 20.04 或 22.04(这是目前车机项目最常用的编译主机系统)、JDK 11、Python 3.8+、以及高通平台专用的编译工具链。内存建议 32GB 以上,因为 Android 系统源码全编一次非常吃资源。
# 安装基础依赖(以 Ubuntu 为例) sudo apt-get install -y openjdk-11-jdk python3 python3-pip \ git-core gnupg flex bison build-essential zip curl \ zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 \ libncurses5 lib32ncurses5-dev x11proto-core-dev \ libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils \ xsltproc unzip fontconfig这些包看着多,但缺一个都可能在编译中途报错。我遇到过因为少了libncurses5导致编译到一半失败的玄学问题,排查了半天才发现是依赖缺失。
3.2 源码集成:把 EVS 模块放进系统编译树
这份 EVS 工程不是独立 APK,它需要集成到 Android 系统源码树里编译。通常的做法是把evs/目录放到vendor/qcom/proprietary/或device/qcom/下,然后在device.mk里加入模块声明。
# 在 device.mk 中添加 EVS 模块 PRODUCT_PACKAGES += \ evs_app \ evs_manager \ evs_hal.default \ evs_config.xml # 添加 EVS 相关的权限配置 PRODUCT_COPY_FILES += \ vendor/qcom/proprietary/evs/config/evs_config.xml:$(TARGET_COPY_OUT_VENDOR)/etc/evs_config.xml这里有个关键点:evs_config.xml必须拷贝到vendor/etc/下,因为 EVS HAL 在初始化时会从这个固定路径读取配置。如果路径不对,HAL 会直接返回空摄像头列表。
3.3 编译与烧录:从源码到车机屏幕
配置好之后,就可以开始编译了。车机项目的编译通常分两步:先编译整个系统镜像,再单独编译 EVS 模块做快速验证。
# 初始化编译环境 source build/envsetup.sh lunch aosp_car_x86_64-userdebug # 根据实际平台选择 # 全量编译(第一次需要,耗时较长) make -j$(nproc) # 单独编译 EVS 模块(后续调试用) mmm vendor/qcom/proprietary/evs/编译完成后,生成的镜像需要通过 fastboot 或高通专用烧录工具写入车机。烧录后第一次启动,建议先通过adb shell检查 EVS 服务是否正常拉起:
# 检查 EVS Manager 服务状态 adb shell dumpsys evs_manager # 查看摄像头枚举结果 adb shell dumpsys evs_manager --list-cameras # 查看内核日志中 V4L2 设备注册情况 adb shell dmesg | grep -i v4l2如果dumpsys输出里摄像头数量为 0,说明 HAL 层没有正确枚举到设备,需要回头检查evs_config.xml和内核驱动。
4. 核心模块代码逐段解析:HAL 层与 Manager 层怎么配合
4.1 HAL 层摄像头初始化:V4L2 设备打开与格式协商
EVS HAL 层的核心任务是打开 V4L2 设备、协商数据格式、启动流传输。这份工程里的v4l2_camera.cpp封装了这些操作。下面这段代码展示了摄像头初始化的关键步骤:
// v4l2_camera.cpp - 摄像头初始化核心逻辑 bool V4L2Camera::open(const std::string& devicePath) { // 1. 打开 V4L2 设备节点 fd_ = ::open(devicePath.c_str(), O_RDWR | O_NONBLOCK); if (fd_ < 0) { ALOGE("Failed to open camera device: %s", devicePath.c_str()); return false; } // 2. 查询设备能力,确认支持视频捕获 struct v4l2_capability cap; if (ioctl(fd_, VIDIOC_QUERYCAP, &cap) < 0) { ALOGE("VIDIOC_QUERYCAP failed"); return false; } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { ALOGE("Device does not support video capture"); return false; } // 3. 设置像素格式(车机常用 YUYV 或 NV12) struct v4l2_format fmt = {}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = config_.width; fmt.fmt.pix.height = config_.height; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_NONE; if (ioctl(fd_, VIDIOC_S_FMT, &fmt) < 0) { ALOGE("VIDIOC_S_FMT failed"); return false; } // 4. 申请 DMA 缓冲区 struct v4l2_requestbuffers req = {}; req.count = config_.bufferCount; // 通常 4-6 个 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd_, VIDIOC_REQBUFS, &req) < 0) { ALOGE("VIDIOC_REQBUFS failed"); return false; } return true; }这段代码的逻辑很直接:打开设备、确认能力、设置格式、申请缓冲区。但有几个参数需要根据实际硬件调整。config_.width和config_.height必须和摄像头实际输出分辨率匹配,否则VIDIOC_S_FMT会返回一个被驱动修改过的格式,导致后续帧解析错位。config_.bufferCount决定了帧缓冲队列的深度,太小会导致丢帧,太大则增加延迟。车机倒车场景对延迟敏感,我一般会设成 4。
4.2 Manager 层流会话管理:如何把帧分发给正确的应用
EVS Manager 的核心职责是管理流会话。每个应用请求摄像头时,Manager 会创建一个EvsStream对象,记录订阅者、帧回调和生命周期。下面这段代码展示了流会话的创建和帧分发逻辑:
// EvsStream.cpp - 流会话管理与帧分发 bool EvsStream::startStream() { // 1. 向 HAL 注册帧回调 auto callback = [this](const EvsFrame& frame) { std::lock_guard<std::mutex> lock(mutex_); // 2. 遍历所有订阅者,分发帧数据 for (auto& subscriber : subscribers_) { if (subscriber->isActive()) { subscriber->onFrameAvailable(frame); } } // 3. 帧引用计数减一,归还缓冲区 frame.release(); }; if (!halCamera_->startCapture(callback)) { ALOGE("Failed to start capture on HAL"); return false; } isRunning_ = true; return true; } void EvsStream::addSubscriber(const sp<EvsSubscriber>& sub) { std::lock_guard<std::mutex> lock(mutex_); subscribers_.push_back(sub); // 如果流已经在运行,立即通知新订阅者 if (isRunning_) { sub->onStreamStarted(); } }这里的关键设计是「帧引用计数」。HAL 层从驱动拿到缓冲区后,不会立即释放,而是等所有订阅者都处理完才归还。如果某个应用处理帧的速度太慢,会拖累整个流的帧率。实际项目中,我见过因为一个应用在主线程做图像处理导致倒车画面卡成 PPT 的案例。解决办法是在onFrameAvailable里只做数据拷贝,把耗时处理放到独立线程。
4.3 JNI 桥接:Java 应用层如何拿到 Native 帧数据
应用层通常用 Java 或 Kotlin 开发,需要通过 JNI 从 Native 层获取帧数据。这份工程里的evs_jni.cpp实现了这个桥接:
// evs_jni.cpp - Java 层获取帧数据的 JNI 接口 static void nativeRequestFrame(JNIEnv* env, jobject thiz, jlong streamHandle, jobject surface) { auto* stream = reinterpret_cast<EvsStream*>(streamHandle); if (!stream) { ALOGE("Invalid stream handle"); return; } // 将 Java Surface 转换为 Native Window ANativeWindow* window = ANativeWindow_fromSurface(env, surface); if (!window) { ALOGE("Failed to get native window from surface"); return; } // 设置帧输出目标为 Surface stream->setOutputWindow(window); // 启动流,帧数据会直接渲染到 Surface stream->startStream(); // 注意:window 引用由 stream 持有,不要在这里 release }这个接口的设计思路是「零拷贝渲染」:Native 层拿到帧后,直接通过ANativeWindow渲染到 Java 层的 Surface,避免在 JNI 层做内存拷贝。参数streamHandle是 Native 层流对象的指针,由 Manager 在创建流时返回给 Java 层。surface是 Java 层SurfaceView或TextureView持有的 Surface 对象。
注意:
ANativeWindow_fromSurface会增加引用计数,必须在流停止时调用ANativeWindow_release,否则会造成内存泄漏。我见过因为这个泄漏导致车机连续运行几小时后 OOM 的案例。
5. 避坑与排查:EVS 调试中最容易翻车的几个点
5.1 摄像头枚举为空:配置文件路径与权限问题
现象:应用启动后调用getCameraList()返回空列表,dumpsys evs_manager显示摄像头数量为 0。
原因:最常见的原因是evs_config.xml没有放到vendor/etc/evs/下,或者文件权限不对(SELinux 拦截)。另一个可能是内核驱动没有正确注册 V4L2 设备节点。
解决:先用adb shell ls -l /dev/video*确认设备节点存在。然后检查adb shell dmesg | grep v4l2看驱动是否报错。最后确认evs_config.xml的 SELinux 上下文是否正确,必要时在file_contexts里添加规则。
5.2 画面撕裂或卡顿:缓冲区数量与帧率不匹配
现象:倒车影像画面出现横向撕裂,或者帧率明显低于摄像头标称帧率。
原因:缓冲区数量太少导致丢帧,或者应用层处理帧的速度跟不上摄像头输出速度。另一个常见原因是 V4L2 的field参数设置错误,导致隔行扫描数据被当成逐行处理。
解决:把bufferCount从默认的 2 增加到 4 或 6。检查VIDIOC_S_FMT返回的实际格式,确认field是V4L2_FIELD_NONE。如果应用层处理慢,把耗时逻辑移到独立线程,确保onFrameAvailable快速返回。
5.3 多路摄像头切换黑屏:流会话未正确释放
现象:从倒车影像切换到环视影像时,屏幕黑屏 1-2 秒才恢复。
原因:切换时没有先停止旧流就启动新流,导致 HAL 层缓冲区冲突。或者旧流的 Surface 没有释放,新流无法绑定。
解决:在切换逻辑里严格按「停止旧流 → 释放 Surface → 创建新流 → 绑定新 Surface → 启动新流」的顺序执行。可以在 Manager 层加一个状态机,确保流状态转换是原子的。
5.4 夜间画面噪点严重:3D 降噪未启用
现象:白天画面正常,夜间或低照度环境下噪点明显,影响倒车判断。
原因:车机摄像头通常支持硬件 3D 降噪,但默认可能未启用。或者 ISP 参数没有针对低照度场景调优。
解决:检查摄像头驱动是否支持V4L2_CID_3A_LOCK或类似的 ISP 控制接口。在高通平台上,通常需要通过 Camera HAL 的vendor_tag设置降噪等级。如果硬件不支持,可以在应用层加轻量级降噪算法,但要注意不能引入太大延迟。
5.5 编译报错找不到 EVS 头文件:依赖顺序问题
现象:mmm vendor/qcom/proprietary/evs/编译时报错fatal error: EvsCamera.h: No such file or directory。
原因:EVS 模块依赖的 HAL 头文件没有先编译,或者Android.bp里的header_libs配置缺失。
解决:先全量编译一次make -j$(nproc),确保所有依赖模块都已生成。然后在Android.bp里显式声明header_libs: ["libevs_headers"]。如果还是找不到,检查LOCAL_C_INCLUDES是否包含了正确的路径。
6. 二次开发改造:从跑通到定制的几个实用技巧
6.1 增加一路摄像头:配置与代码的同步修改
这份工程默认支持 4 路摄像头(前、后、左、右)。如果你需要增加第 5 路(比如流媒体后视镜),需要同时修改三个地方:evs_config.xml里增加摄像头节点、HAL 层确认 V4L2 设备节点存在、Manager 层更新摄像头枚举逻辑。我一般会先在evs_config.xml里加配置,然后通过dumpsys evs_manager --list-cameras验证 HAL 是否能枚举到新设备,最后再改应用层 UI。
6.2 自定义画面拼接:环视算法的接入点
环视拼接通常在应用层或独立的 GPU 处理模块完成。如果你想替换默认的拼接算法,接入点在EvsStream的帧回调之后、渲染之前。常见做法是创建一个ImageProcessor接口,在onFrameAvailable里调用处理链。注意处理链不能阻塞帧回调线程,否则会拖累整个流的帧率。
6.3 延迟优化:从 200ms 降到 80ms 的实践
倒车影像的延迟直接影响用户体验。我做过一轮优化,把端到端延迟从 200ms 降到了 80ms 左右。关键措施包括:把bufferCount从 6 降到 4、关闭不必要的格式转换、把应用层渲染从TextureView换成SurfaceView、以及把帧回调线程优先级提到THREAD_PRIORITY_URGENT_DISPLAY。每项优化单独测试,确认没有引入画面撕裂再叠加下一项。
6.4 验证方法:用 dumpsys 和 systrace 定位瓶颈
改造之后怎么验证?我习惯用两个工具:dumpsys evs_manager看流状态和帧计数,systrace看帧回调线程的耗时分布。如果dumpsys显示帧计数增长正常但画面卡顿,问题多半在渲染层;如果帧计数增长缓慢,问题在 HAL 或驱动层。systrace里重点看EvsStream::onFrameAvailable的耗时,超过 5ms 就需要优化。
从那以后我每次改完 EVS 相关代码,都强制走一遍「dumpsys 看状态 → systrace 看耗时 → 实车验证延迟」的流程,再也不敢跳过任何一步。希望帮到你。
本文还有配套的精品资源,点击获取