首页 / 资讯中心 / 文章详情

Android车载显示开发实战:多屏映射、SystemUI适配与性能调优

Android车载显示开发实战:多屏映射、SystemUI适配与性能调优 ★ FEATURED ARTICLE
做Android车载显示这个方向之前我在手机应用上写了好几年界面自认为对Window、View、渲染这些东西都门儿清。真到了车机项目才发现同一个Android完全不是同一个物种。这是系列的第二篇上一篇我们把Android Automotive OS的开发环境、模拟器和整体框架跑通了这一篇专门讲显示多屏怎么映射、SystemUI怎么适配、启动性能和流畅度怎么抠、实车调试怎么干。内容偏工程实践适合正在做车载显示系统应用、或准备从手机应用转过来的工程师参考。文中提到的项目细节我都做了脱敏处理但每个坑都是真实踩过的。1. 车载显示与手机显示的分水岭先弄明白车机为什么要搞一套独立显示体系1.1 车机系统显示层次比手机多了半层安全语义手机上的Android所有窗口基本是平权的Activity可以随便嵌套、悬浮、切换系统把焦点给谁谁就站在最前面。车载环境完全不是这样。车机上有一类内容是和驾驶安全强相关的车速、挡位、故障告警、倒车影像。这些内容如果被一个普通App窗口遮住或者被用户误触关闭就是安全事故。所以Android Automotive在显示架构里不是简单地把窗口叠来叠去而是把显示区域和窗口容器做了很严格的层级划分。我最早看dumpsys window windows的时候发现车机上窗口的DisplayId、type、region都有额外的策略限制很多App的窗口根本不允许覆盖某些系统安全区域。这套东西在手机AOSP里虽然也有雏形但真正启用是在车机版本里。最典型的例子是倒车影像RVC。倒车时整车控制器通过CAN告诉车机车机系统会在极短时间内把中控显示切换到倒车画面。这个切换不是启动一个Activity而是在对应Display上叠加一层专用Surface通过HWC的硬件overlay直接送去屏幕绕开普通App窗口调度。也就是说屏幕上优先级最高的一层不是你的App、不是Launcher是这条独立的安全显示链路。理解了这一点很多车机上的怪异现象就解释通了为什么倒车时自定义View会瞬间不可见为什么某些窗口切后台会黑一帧。1.2 一块屏、两块屏、三块屏各自扮演的角色完全不同做车载显示先得把屏幕角色捋清楚。传统座舱至少有三类屏幕分成中控、仪表、HUD/副驾屏它们在Android系统里的地位和实现方式天差地别。我列个表格方便你快速建立认知屏幕交互方式安全等级显示内容Android中的实现中控屏触摸/旋钮中导航、音乐、空调、倒车、应用列表通常是primary display走常规WMS窗口仪表屏无触摸高车速、转速、挡位、告警、导航箭头通常是独立display走Cluster专用渲染链路HUD/副驾屏部分触摸低/中车速投影、娱乐视频、车辆状态可能是独立display或virtual display这三块屏可能接在同一个SoC上也可能由不同SoC驱动。我见过很多项目是座舱域控制器带着中控仪表Android系统里中控是主屏仪表是一块逻辑Display通过DRM的多个encoder输出。副驾屏和后排娱乐屏则更倾向于用virtual display把应用内容投到特定物理接口上。开发时最容易犯的错是把手机那套多窗口自由切换直接搬到仪表屏上。仪表屏没有触摸而且不允许用户随便关窗所以系统会在仪表Display上锁定窗口普通应用根本无法在仪表区域创建窗口。你在仪表上看到的任何内容几乎都是由系统侧的ClusterService统一渲染的不是某个普通App的View。1.3 为什么车载显示必须锁定部分窗口车机在行驶中中控如果允许用户随便切到桌面、退出导航这体验绝对是灾难。所以Android Automotive引入锁任务机制系统可以指定某些应用常驻前台setLockTaskPackages和startLockTask()就是干这个的。导航、仪表、通话这类关键显示必须常驻不能因为用户误触Home键就跑掉。这个设计对开发者的直接影响是你的应用不能假设自己什么时候能拿到焦点也不能假设窗口一直在。车机系统随时可能因为安全策略把某个应用窗口降到后台或者把一个系统级覆盖层插到你的应用上面。很多从手机转过来的同事第一周都会骂为什么我的Activity在车机上退到后台时连onStop都不好好走因为系统根本没让窗口正常走完生命周期直接以更粗暴的方式把Surface拿走了。2. 多屏与异形屏的显示映射SurfaceFlinger那层到底在忙什么2.1 多个Display在车机里是怎么建立的车机系统启动时Kernel的DRM/KMS驱动会枚举每一个物理显示接口一个connector对应一块屏。SurfaceFlinger读取这些信息后给上层分配逻辑Display Id。开发者层面能感知到的是DisplayManager里的Display对象以及dumpsys SurfaceFlinger里那一串Display列表。如果你要在车机上真正加一块物理屏不是在自己App里createVirtualDisplay就行而是要和BSP驱动同事一起在设备树/内核cmdline里把第二个显示接口的video参数、时序、分辨率全都配好让SurfaceFlinger能枚举出第二块物理Display。现代座舱SoC大多支持额外接一个DSI或者DP接口跑起来就是中控仪表两块物理屏。但实际做多屏业务时更多场景是用virtual display。比如副驾屏想跑一个视频App但不想让它和中控的导航抢焦点就用VirtualDisplay把内容渲染到一个独立Surface再由厂商中间层投到副屏物理接口上。这个方案对App透明开发快但有代价virtual display的vsync和物理屏不一定对齐画面容易撕裂延迟也偏高。我踩过的坑是副驾屏播放视频时周期性地抽动查到最后就是virtual display和物理屏刷新节奏不一致后来让BSP把副驾屏也做成独立物理Display问题才消停。2.2 分辨率、dpi和异形屏适配不是塞一个宽高就行车机屏型千奇百怪最常见的就够你喝一壶长条仪表屏1920x720、中控竖屏1080x1920、还有圆形仪表甚至曲面屏。很多人按手机targetScreen那套来做控件全部变形字小得看不清。根因是密度值设置不对。我举个例子。一块10.25英寸的1920x720长条屏对角线像素约2052除以10.25英寸物理dpi大约200。如果你把系统密度强行设成400dpi那所有dp尺寸的控件物理大小直接缩一半本来该有两个手指宽的按钮变成拇指尖大小行车中根本没法点。正确做法是把密度贴近物理真实值然后靠sw600dp、sw720dp这种限定符去区分横竖屏和长条形态不要指望一套dpi跑天下。异形屏还有两个隐藏问题圆角和摄像头区域。车机中控现在很多带圆角、带挖孔或者带仪表遮罩区内容如果不做安全区裁剪边缘的按钮会被物理边框吃掉。系统一般会提供DisplayCutout信息但车机厂商的定制ROM经常没把这些字段填对导致cutout区域在车机上形同虚设。我的习惯是直接让UI设计师把核心交互控件收敛到屏幕中心安全区不做刘海贴边。2.3 仪表专用显示链路Cluster和普通App毫无关系这一点我每次带新人都会反复强调仪表屏上看到的内容和你的Android Activity渲染几乎没关系。Android Automotive里有一个CarInstrumentClusterManagerApp可以通过它下发导航指令、箭头、里程信息但真正把这些信息画到仪表屏幕上的是系统ClusterService它运行在完全独立的渲染上下文里。也就是说你想在仪表上放一个自定义View来显示转速表没门儿。系统安全策略不允许普通应用直接往仪表Display投递任意窗口。很多中控App开发抱怨我的导航箭头为什么仪表上不显示就是因为只在Activity里把那几个像素画出来了根本没有通过跨进程接口把转向指令发给ClusterService。这是架构问题不是画图问题。开发车载显示我建议一开始就把仪表数据和仪表渲染想成两个领域。App负责采集和上报高质量数据Cluster侧负责按照车规流程把它画出来。只有系统应用、有签名权限的才能直接和ClusterService对话普通第三方应用连接口都摸不到。3. SystemUI与车载应用适配状态栏、协调布局、进度条的车规改法3.1 状态栏和导航栏该收就收该留就留车机的SystemUI和手机长得有点像但行为逻辑完全不同。手机上你做一个沉浸式看视频App隐藏状态栏很合理车机上如果你把状态栏一藏时间、网络、空调温度这些重要信息也跟着没了司机在行驶中想看一眼时间都找不到地方这就不安全了。我见过一个项目导航App为了更沉浸把全屏沉浸写法直接抄过来结果系统的一个安全悬浮按钮也被顶掉了导致用户在高速上无法一键呼出360影像。后来我们定了规矩信息娱乐类页面原则上不隐藏系统状态栏如果确实要全屏必须保留一个可再唤起状态栏的物理按键或边缘手势并且等系统确认安全之后才允许隐藏。所以车载开发者的心态要先转变显示沉浸感在车上优先级低于功能可见性。你做一个地图页面导航箭头、车速、剩余里程这些元素比背景好不好看重要得多。3.2 CoordinatorLayout加Banner在车机上的异地登录手机端用CoordinatorLayout配合Banner做折叠头图再嵌套列表滚动几乎是各种App首页的标配。移植到车机上第一个大坑就是焦点模型。车机很多车型不是全触摸而是靠旋钮、方向盘按键控制焦点。焦点默认情况下会落在第一个可聚焦控件上。当你的Banner是一个可滚动的轮播容器时旋钮旋转事件会被Banner吃掉焦点就在Banner的多个子项之间来回跳列表里的核心内容反而没法操作。我在项目里就遇到过司机用旋钮想选中列表里的音乐结果页面上横幅广告不断切换焦点在里面鬼打墙。还有一个问题Banner轮播的自动切换动画在行驶中会不断变化屏幕局部内容吸引司机余光增加驾驶分心风险。车规上对这种动态内容是有排斥的。我的做法是把Banner做成静态位图车辆挂P挡时才开始轮播D挡立刻停止切换。如果你实在要用协调布局的联动效果一定要把联动滚动逻辑限定在触摸输入旋钮按键事件直接跳过Banner区域。3.3 进度条在车上不是转圈就行手机App加载中放个转圈用户顶多骂两句。车机上如果出现一个全屏无底进度条等于直接告诉驾驶员系统瘫痪了。尤其导航加载、倒车切换、音频恢复这几个场景每多一秒无反馈都是安全隐患。车机加载核心原则是不给没有确定性的等待。能显示百分比就不要转圈能显示缓存数据就不要清空界面。我一个很典型的改进案例地图启动时原本是白屏转圈3秒改成启动瞬间先绘制一张上次离线缓存的地图底图同时后台拉取最新数据并显示一条横向进度条正在更新路况剩余12项。用户可感知的无内容时间从3秒降到接近0。技术上我现在更倾向用LinearProgressIndicator的determinate模式配合数据加载任务实时更新进度而不是用MaterialProgressBar无限转圈。进度条的位置也要避开仪表侧边缘和安全区域避免余光一直被闪烁动画干扰。还有一点不要在所有页面都放进度条。画面内已经有大量静态信息时一个细小的环形指示就够把注意力留给驾驶。4. 启动性能与显示流畅度从开机第一帧到列表滚动的每一个毫秒4.1 冷启动显示链路黑屏时间怎么压车载中控冷启动完整路径是Bootloader→kernel→init→zygote→system_server→SurfaceFlinger→Launcher。用户真正看到中控UI得等Launcher起来并且完成第一帧绘制。目前座舱域项目里从整车上电到中控桌面可操作车企要求普遍在4秒到6秒高端项目要求4秒以内。这个要求非常紧。优化启动显示顺序很重要。第一是提前点亮屏幕Kernel阶段就让DisplayPanel工作Android起来之后系统UI的第一帧不会卡在等待背光。第二是减少黑屏白屏时间Panel自身的上电时序往往是黑屏时间最大的不可控变量这需要BSP驱动配合反复调。第三是把Launcher放进persistent进程并放进开机自启动白名单、提高进程优先级否则等zygote一个个孵化普通App桌面要等很久。第四是首帧渲染复杂度必须低首帧只画静态底图和当前页骨架其余Tab、Banner、列表全部延后加载。我实测过一个项目优化前冷启动到桌面6.8秒优化后4.3秒。大头就出在把Launcher进程优先级提前和面板上电时序调整这两项上第二帧之后的页面绘制反而没怎么动。4.2 卡顿、掉帧定位相信数据不要相信肉眼车载显示掉帧排查工具和手机一模一样adb shell dumpsys gfxinfo package看App侧帧统计adb shell dumpsys SurfaceFlinger --latency layer看合成侧丢帧。但在车机上很多卡顿根因不在App代码而在显示合成链路。我处理过一个问题中控列表滑动掉帧60Hz屏幕本该16.6ms一帧实测帧间隔跑到33ms。CPU和主线程都没压力最后查出来是GPU带宽满载——那块车机屏是4K60 HDR页面里多个surface带半透明效果还有一个全屏实时模糊背景HWC无法拆成多个overlay layer只能全部丢给GPU合成。一帧要合成这么多特效带宽直接被打满。解决办法是去掉全屏模糊把能设成opaque的Surface尽量设成opaque让HWC走硬件overlay帧间隔立刻回到16.6ms。这件事让我学会了一句话显示优化不止是动画代码优化还是合成策略优化。另外要说内存。车机进程一旦被LMKD杀掉用户感知就是当前界面闪黑、回到桌面、重新启动App。把导航这种关键App做成不可杀需要在lmkd配置里对关键进程设置protection同时从源头压内存占用。像图片多、缓存多的显示类场景用Bitmap复用和三缓冲策略能省出不少内存这也会直接影响显示稳定性。4.3 多屏vsync与刷新率撕裂往往不是App的锅多屏车机还有一个手机场景很难遇到的坑主屏和副屏刷新率不一致时的画面同步问题。SurfaceFlinger默认以主屏vsync为基准副屏如果刷新率不同App生成的帧可能来不及在副屏垂直消隐期完成交换结果就是撕裂和周期性抽动。现在的Android版本支持per-display vsync但很多车机项目并没有在BSP层把副屏的vsync源和刷新率配好。我参与的项目里副驾屏是60Hz中控是60Hz还好等到仪表屏改成120Hz后中控App的动画偶尔出现半屏撕裂查到最后是SurfaceFlinger把仪表的高频vsync当成了全局节奏中控侧的渲染缓存跟不上。这个问题的处理基本在BSP和SurfaceFlinger配置层面。作为应用层开发能做的就是确认你的Surface是否和物理Display绑定避免在virtual display上执行对时序敏感的动画因为virtual display没有可靠的物理vsync保障。5. 权限与外设协同显示蓝牙、USB串口、文件访问这些隐藏雷区5.1 权限清单别照搬手机运行时权限弹窗在车机上很危险车载应用开发有一个非常常见但又很隐蔽的坑直接把手机项目的AndroidManifest.xml权限列表搬过来。结果应用在车机上跑起来正当用户操作时系统突然弹出一个允许获取位置信息吗的对话框。司机在开车怎么点所以在车机项目里运行时权限弹窗基本是禁区。所有需要的危险权限要么在出厂阶段做预授权要么通过系统级签名应用做白名单配置。如果你的应用走三方市场渠道必须在Manifest里声明权限的同时和车厂确认权限授予策略。还有一个Android 11之后的大坑/sdcard/Android/data目录访问限制。很多车机预装App用FileProvider提供文件路径类似content://...fileprovider/external_path/android/data/...但Android 11开始普通App无法用File类直接访问另一个App的Android/data目录。车机上因为涉及系统级签名应用和SAF框架的定制行为更是五花八门。我遇到过文件管理器打开图片预览直接报No such file的问题最后查出来就是厂商定制SAF没按新版本的存储限制适配。开发车载显示文件相关功能前一定先确认目标Android版本和厂商的文件访问策略。5.2 蓝牙状态和显示联动不要直接监听系统蓝牙车机屏幕上蓝牙已连接某某手机的状态展示是一个典型的显示联动功能。但如果你直接监听系统的BluetoothAdapter.ACTION_ACL_CONNECTED大概率会翻车。因为车载蓝牙栈往往不是Android侧的Bluetooth协议栈而是独立的蓝牙MCU/蓝牙SoC在管Android只通过抽象接口拿到部分状态。正确做法是通过CarBluetoothManager这类车辆抽象层去获取蓝牙连接状态同时监听整车蓝牙模块上报的状态变化。Android侧状态和整车蓝牙模块状态不一致时表现就是界面显示已连接但实际音频通路没建立或者断开车载蓝牙后界面还停留在连接中。我写这段是想提醒车载显示的内容本质上是整车多个模块状态的投影。你的界面状态机要以整车状态为准而不是以某一个App内部的落点状态为准。5.3 USB串口调试显示出问题时屏幕帮不了你做车载显示串口调试是绕不开的求生技能。很多系统级的显示问题比如SurfaceFlinger挂掉、屏完全黑掉、Launcher起不来屏幕是不会给你任何线索的。这时候一条UART串口线就是唯一的救命通道。车机上通常会引出调试串口波特率常见115200或1500000。通过串口可以看Kernel、init、SurfaceFlinger的启动日志定位是panel驱动没起来还是SurfaceFlinger初始化失败。我在项目里用串口抓过一次开不了屏的问题一开机日志里全是failed to find panel直接定位到设备树panel节点配置错误省了一下午的瞎猜。USB口方面常见的是U盘文件显示和调试连接。用SAF框架打开U盘文件时要小心车机厂商只做了部分MIME过滤图片格式明明支持却在文件选择器里显示灰的。真要做深度文件预览建议走MediaStore API而不是老旧的Environment.getExternalStorageDirectory()。分屏、多用户、多Display环境下老API拿到的路径经常不准显示出来的往往不是你插入的那块U盘。6. 从Android Studio到实车调试链路与几个让我印象深刻的实战问题6.1 AAOS模拟器能信几分不能信几分Android Studio里创建Automotive Emulator日常开发逻辑、UI布局、基本交互验证都能干。AAOS模拟器也支持多Display模拟但和实车差距还是很大模拟器没有真正的HWC多屏overlay仪表Cluster链路根本模拟不出来触摸坐标映射和panel时序也和真机无关。我的工作流是代码逻辑和UI在模拟器里过涉及多屏、启动性能、安全链路、触摸偏差的一定实车验收。曾经有个动画在模拟器上丝滑流畅实车上因为缺少GPU带宽直接掉到20帧这种事只有实车才能暴露。模拟器还有一个常见问题wm size和wm density改了之后很多布局在模拟器上看正常但实车因为物理尺寸不同控件显得过大或过小。所以模拟器适合定逻辑不适合定视觉尺寸。6.2 实车调试的日常命令先看窗口再看图层最后看数据实车调试显示问题我一般按这套顺序操作adb devices adb shell dumpsys window windows adb shell dumpsys SurfaceFlinger --list adb shell dumpsys SurfaceFlinger --latency layer adb shell wm size adb shell wm density adb shell dumpsys activity top黑屏时第一件事看SurfaceFlinger里有没有那个Surface。Surface不存在查App进程为什么没起来Surface存在但黑查App渲染侧有没有真正提交帧Surface正常但画面被盖住查窗口层级和系统安全窗口。窗口错位就看dumpsys window里bounds和DisplayId对不对。触摸偏差就用getevent对比触摸上报坐标和屏幕显示坐标。实车另一个常见情况倒车或摄像头抢占画面后你的App窗口被系统置为不可见回来时闪黑一帧。原因是抢回窗口时Surface被重建了。处理办法是监听onWindowFocusChanged在焦点恢复前不要销毁绘制Surface保留最后一帧缓存用来过渡。这个体验问题在实车调试里特别典型模拟器完全复现不了。6.3 显示异常根因排查清单这些是我在项目里沉淀下来的排查表遇到问题可以先对照现象常见根因排查方向中控黑屏但声音正常SurfaceFlinger或App未绘制查gfxinfo、surface状态、App是否存活仪表显示错乱、残留Cluster渲染链路问题查ClusterService日志和GPU合成触摸偏移或反向显示屏与触摸屏坐标映射错误getevent对比坐标调HAL校准花屏、条带HWC overlay/DRM mode配置问题检查带宽、format尝试禁用overlay副屏撕裂、延迟vsync不同步或virtual display改为物理Display或启用per-display vsync应用被杀瞬间闪黑LMKD内存压力调整omm adj保护关键进程返回界面闪一帧黑安全窗口抢Surface保留缓存帧过渡避免Surface重建最后再做一个小补充车载显示和手机显示最本质的差别是它永远服务于正在开车的人。你做的每一个动画、每一次隐藏状态栏、每一个进度条样式都要先在脑子里过一遍这个画面在驾驶员余光里会是什么效果。我这些年最大的体会是显示工程师在车机项目里不只是写代码还要和整车交互、安全策略、硬件BSP甚至结构设计反复拉齐。如果你真要入门车载显示我建议先找一个实车启动黑屏的问题盯几天那比看一百篇框架文章都管用。
阅读完成 · 觉得有帮助?
咨询建站