一、车载显示系统定制:从一块屏到多块屏的进化

说实话,我第一次接触车载Android系统时,最让我头疼的就是显示系统。手机平板上的那套逻辑,到了车上完全不够用。你想想看,手机就一块屏,顶多分个屏。但车上呢?仪表盘、中控、副驾娱乐屏、后排头枕屏……少则两三块,多则七八块。

我当年参与的第一个车载项目,客户要求仪表和中控独立显示,但又要能互相投射。结果呢?系统直接崩溃,日志里全是SurfaceFlinger的报错。嗯,从那以后,我就开始深入研究DisplayManagerService了。

1.1 DisplayManagerService:车载多屏的基石

DisplayManagerService(简称DMS)是Android显示系统的核心服务。它管理着所有物理显示设备和虚拟显示设备。在手机上,DMS通常只管理一块内屏和可能的投屏外屏。但在车上,情况完全不同。

DMS的核心职责包括:

  • 显示设备枚举:检测并注册所有物理显示设备
  • 显示策略管理:决定哪个应用显示在哪块屏上
  • 显示模式切换:处理分辨率、刷新率、旋转等变化
  • 虚拟显示创建:为屏幕投射、录屏等场景提供支持

关键知识点:车载系统中,每块物理屏对应一个Display对象,通过DisplayId来区分。仪表通常是DisplayId=0,中控是DisplayId=1,以此类推。

我在项目中遇到过一个问题:系统启动时,仪表屏和中控屏的初始化顺序颠倒了。结果仪表上显示了中控的Launcher,中控上显示了仪表盘。这要是交付给客户,后果不堪设想。后来我通过修改init.rc中的服务启动顺序,配合DMS的显示设备优先级配置,才彻底解决。

1.2 多屏显示架构:仪表/中控/后排的协同

车载多屏显示,说白了就是让不同屏幕各司其职,又能协同工作。我习惯把车载屏幕分为三类:

屏幕类型 典型用途 显示特性
仪表屏 车速、转速、导航指示 高刷新率、低延迟、常亮
中控屏 导航、音乐、设置 高分辨率、触控交互
后排屏 视频播放、游戏 独立音视频、可休眠

要实现多屏协同,核心在于显示策略(DisplayPolicy)的定制。Android原生的显示策略只考虑单屏,我们需要重写它。

// 车载多屏显示策略示例
public class CarDisplayPolicy extends DisplayPolicy {
    @Override
    public int getPreferredDisplayId(Intent intent, ActivityInfo activityInfo) {
        // 根据Intent和Activity信息,决定显示在哪块屏上
        if (isInstrumentClusterApp(activityInfo)) {
            return Display.DISPLAY_INSTRUMENT_CLUSTER; // 仪表屏
        } else if (isRearSeatApp(activityInfo)) {
            return Display.DISPLAY_REAR_SEAT; // 后排屏
        }
        return Display.DEFAULT_DISPLAY; // 默认中控屏
    }
}

这里有个坑:千万别把所有应用都往仪表屏上放。仪表屏对安全要求极高,只能显示经过认证的仪表应用。我曾经见过一个项目,第三方应用通过某种方式跑到了仪表屏上,结果导致仪表显示异常,差点引发安全事故。

安全警告:仪表屏的显示内容必须经过严格审核,建议使用白名单机制,只允许系统级或签名的应用在仪表屏上显示。

1.3 屏幕投射与镜像:从技术到体验

屏幕投射和镜像,是车载多屏系统里最常用的功能之一。比如,把中控的导航投射到仪表屏上,或者把手机内容镜像到车载大屏上。

Android提供了两种实现方式:

  • VirtualDisplay:创建一个虚拟显示,将内容渲染到Surface上,再通过MediaCodec编码传输
  • DisplayManager.createDisplay:直接创建一个逻辑显示,绑定到物理屏或虚拟屏上

我个人更推荐使用VirtualDisplay方案,因为它更灵活,而且对系统侵入性小。但要注意性能问题——编码解码会消耗大量CPU/GPU资源。

// 创建虚拟显示用于屏幕投射
private void createVirtualDisplayForProjection() {
    DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
    VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("car_projection", 1920, 1080, 320)
            .setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC)
            .build();
    VirtualDisplay vd = dm.createVirtualDisplay(config, surface, null);
    // 将虚拟显示的内容编码并发送到目标屏幕
}

我记得有一次,客户要求实现中控到后排的屏幕镜像,延迟要低于100ms。一开始用软件编码,延迟在200ms左右,画面还卡顿。后来改用硬件编码器(MediaCodec的SurfaceInput方式),延迟降到了50ms以内。嗯,这里的关键是绕过GPU渲染,直接使用显示硬件

实战技巧:对于车载屏幕投射,建议使用H.264硬件编码,码率控制在10-20Mbps,分辨率根据目标屏幕自适应。如果目标屏幕是720p,就没必要传1080p的数据,浪费带宽还增加延迟。

1.4 分辨率与刷新率适配:让每块屏都完美

车载屏幕的规格五花八门。有的仪表屏是1280x480的细长条,有的中控屏是1920x1080的方形屏,还有的后排屏是3840x2160的4K屏。刷新率也从30Hz到120Hz不等。

Android的WMS(WindowManagerService)和SurfaceFlinger需要知道每块屏的物理参数,才能正确渲染。这些参数存储在display_device.xml或通过HAL层上报。

我建议在系统启动阶段,通过读取EDID(扩展显示识别数据)或设备树(Device Tree)来获取屏幕参数。如果EDID数据不可靠,可以在overlay目录下配置固定参数。

// 在DevicePolicyManager中配置屏幕参数
<display id="0" type="physical">
    <mode width="1280" height="480" refreshRate="60" />
    <mode width="1280" height="480" refreshRate="30" />
</display>
<display id="1" type="physical">
    <mode width="1920" height="1080" refreshRate="60" />
</display>

这里有个容易踩的坑:刷新率不匹配导致画面撕裂。比如仪表屏是60Hz,中控屏是30Hz,如果两个屏显示同一个动画,就会出现一边流畅一边卡顿的现象。我的解决方案是:为不同屏幕创建独立的Choreographer实例,让每个屏幕按自己的节奏刷新。

核心原则:每块屏幕都应该有独立的显示管道(DisplayPipe),包括独立的VSync信号、独立的BufferQueue、独立的渲染线程。不要试图用一块屏的VSync去驱动另一块屏。

1.5 知识体系总览

下面这张图,是我梳理的车载显示系统定制核心逻辑。你可以把它当作本章的思维导图。

车载显示系统定制核心架构 DisplayManagerService 多屏显示架构 屏幕投射与镜像 分辨率与刷新率 显示策略定制 安全与白名单 目标:每块屏独立、安全、高效地显示内容

这张图展示了车载显示系统定制的五大核心模块。DMS处于中心位置,向上承接应用层的显示请求,向下管理物理显示设备。多屏架构、投射镜像、分辨率适配、显示策略和安全机制,这五个方面缺一不可。

好了,关于车载显示系统定制,核心内容就是这些。记住一句话:车载显示不是简单地把手机那套搬过来,而是要从硬件到软件,从策略到安全,重新设计一套适合多屏场景的显示体系。我在后续的项目中,一直沿用这套思路,基本没出过大问题。


公众号:蓝海资料掘金营,微信deep3321