4. 系统软件架构:Android/Linux双屏显示架构

好,咱们进入系统软件架构这一块。说实话,双屏异显这个功能,硬件只是基础,真正的灵魂在软件层。你硬件连好了,驱动也调通了,但系统怎么知道哪个屏幕该显示什么?怎么管理两个屏幕的合成?这就是架构要解决的问题。

我个人习惯把双屏显示架构分成两大阵营来看:Android阵营纯Linux阵营。这两套体系,思路完全不同,但目标是一样的——让两个屏幕各司其职。

4.1 Android双屏显示架构

Android这边,核心是SurfaceFlinger。你可以把它理解成「显示管家」。所有应用的界面,最终都要交给SurfaceFlinger来合成,然后输出到屏幕。

双屏场景下,SurfaceFlinger怎么工作?我简单梳理一下流程:

  1. 每个应用创建自己的Surface,往上面画内容。
  2. SurfaceFlinger拿到这些Surface,根据窗口的Z-order(层级顺序)进行合成。
  3. 合成完成后,输出到对应的显示设备(Display Device)。

关键点来了:Android从5.0开始就原生支持多Display。每个Display有独立的合成上下文,包括独立的帧缓冲区、独立的Vsync信号。这意味着,你可以把不同的应用分配到不同的屏幕上。

核心概念:Display ID

每个物理屏幕在Android系统中都有一个唯一的Display ID。主屏通常是0,副屏是1、2...以此类推。应用可以通过DisplayManager来查询和选择要显示的屏幕。

我在项目中遇到过一个问题:副屏显示的内容总是闪烁。查了半天,发现是副屏的Vsync信号没有和主屏同步。后来在SurfaceFlinger的配置文件中,手动指定了副屏的Vsync源,问题才解决。嗯,这里要注意,双屏的Vsync同步是个坑,后面我会专门讲。

4.2 Linux双屏显示架构

Linux这边,情况稍微复杂一些。传统的Linux显示架构是X Window System,但现在已经逐渐被Wayland取代。在嵌入式仪表盘领域,Wayland是主流选择。

Wayland的核心思想是「每个窗口自己管理自己的缓冲区」。它不像X11那样有一个中心化的Server来管理所有窗口。Wayland的Compositor(合成器)只负责一件事:把各个窗口的缓冲区合成到屏幕上。

双屏场景下,Wayland的Compositor需要管理多个输出(Output)。每个Output对应一个物理屏幕。Compositor会为每个Output维护一个独立的合成列表。

我的经验:在Linux上做双屏,我建议直接用Weston(Wayland的参考实现)。它原生支持多Output配置,而且代码结构清晰,适合二次开发。我曾经在一个项目里,基于Weston做了定制化的双屏显示方案,从底层到上层全部自己控制,效果非常好。

4.3 SurfaceFlinger vs Wayland:核心对比

这两个东西,虽然都是做显示合成的,但设计哲学完全不同。我整理了一个对比表格,方便你理解:

对比维度 SurfaceFlinger (Android) Wayland (Linux)
架构模式 中心化合成 去中心化合成
缓冲区管理 由SurfaceFlinger统一管理 由客户端(窗口)自行管理
多屏支持 原生支持,通过Display ID区分 原生支持,通过Output对象区分
窗口管理 由WindowManagerService控制 由Compositor控制
性能特点 适合复杂UI场景,但延迟稍高 延迟低,适合实时性要求高的场景
典型应用 车载娱乐系统、手机 仪表盘、工业控制

你想想看,为什么Android要用中心化架构?因为Android的UI场景太复杂了——各种动画、过渡、多窗口叠加。中心化合成可以更好地管理这些复杂场景。而Wayland选择去中心化,是因为它更关注性能和低延迟,适合对实时性要求高的嵌入式场景。

4.4 窗口管理策略

双屏显示,窗口管理是个大问题。你不能让两个屏幕的窗口乱跑。我总结了几种常见的策略:

策略一:独立窗口栈

每个屏幕维护独立的窗口栈。主屏上的窗口不会跑到副屏上,反之亦然。这种策略实现简单,但灵活性差。适合仪表盘+中控这种固定分工的场景。

策略二:全局窗口栈 + 屏幕映射

所有窗口在一个全局栈里管理,但每个窗口可以指定要显示在哪个屏幕上。这种策略灵活,但实现复杂。适合需要跨屏拖拽窗口的场景。

策略三:混合策略

部分窗口固定在某一个屏幕,部分窗口可以跨屏。比如,导航应用可以跨屏显示,但仪表盘应用只能显示在主屏。这种策略最实用,也是我推荐的做法。

避坑指南:我曾经在一个项目中,采用了全局窗口栈策略。结果副屏上的窗口被主屏的弹窗遮挡了。原因是窗口的Z-order是全局的,主屏的弹窗Z-order更高,覆盖了副屏的窗口。后来我改成独立窗口栈,问题才解决。所以,如果你的两个屏幕显示的内容完全独立,一定要用独立窗口栈

4.5 实际项目中的架构选择

说了这么多,到底怎么选?我个人的建议是:

  • 如果你的系统是Android:直接用SurfaceFlinger + WindowManagerService。Android已经帮你做好了多屏支持,你只需要配置好Display ID和窗口策略就行。
  • 如果你的系统是纯Linux:用Wayland + Weston。Weston的配置非常灵活,你可以通过配置文件指定每个Output的分辨率、位置、旋转等参数。
  • 如果你需要混合架构:比如Android跑应用,Linux跑仪表盘。那就要考虑双系统通信的问题了。我一般用虚拟显示的方式,把Linux的显示内容作为Android的一个虚拟Display,然后由SurfaceFlinger统一合成。

嗯,这部分内容比较多,但都是干货。你先把架构理清楚,后面讲具体实现的时候,就会轻松很多。