3、软件框架解析:Android/Linux显示框架、SurfaceFlinger与Wayland、双屏异显与同显原理
好,咱们进入第三章。这一章我打算把多屏系统的软件框架彻底讲透。说实话,很多做硬件的同事觉得软件是黑盒,做软件的又觉得硬件是铁板一块。其实两边打通了,问题就好解决。
我个人习惯把显示框架分成两大阵营:Android 的 SurfaceFlinger 和 Linux 的 Wayland。虽然底层都是 DRM/KMS,但上层调度逻辑完全不同。咱们一个一个来拆。
3.1 Android 显示框架:SurfaceFlinger 的核心逻辑
Android 的多屏显示,核心就是 SurfaceFlinger。你可以把它理解成一个「图层合成器」。每个 App 画好自己的画面,交给 SurfaceFlinger,它再把这些图层叠在一起,输出到屏幕。
我在项目中遇到过一个问题:双屏显示时,副屏画面卡住不动。查了半天,发现是 SurfaceFlinger 的合成策略没配置对。嗯,这里要注意。
SurfaceFlinger 的工作流程大致如下:
- App 通过 BufferQueue 提交图形缓冲区
- SurfaceFlinger 收到 VSync 信号后开始合成
- 合成结果通过 Hardware Composer (HWC) 送到显示设备
关键点在于 HWC。它是个硬件抽象层,负责把合成工作交给 GPU 或者显示控制器。如果 HWC 不支持某个图层组合,SurfaceFlinger 就会用 GPU 做软件合成——性能会掉一大截。
核心概念:Android 的「显示设备」在代码里叫 DisplayDevice。每个物理屏幕对应一个 DisplayDevice,每个 DisplayDevice 有自己的合成器。双屏异显,就是两个 DisplayDevice 各自独立工作。
3.2 Linux 显示框架:Wayland 与 DRM
Linux 这边,传统 X11 已经逐渐被 Wayland 取代。Wayland 的设计哲学和 SurfaceFlinger 有点像,但更轻量。
Wayland 的核心是 Compositor。它直接和 DRM (Direct Rendering Manager) 打交道。DRM 是内核里的显示驱动框架,负责管理显存、模式设置、页面翻转。
说白了,Wayland 就是「每个客户端自己画自己的窗口,Compositor 只负责把它们拼起来」。这和 X11 那种「所有窗口都经过 X Server 转发」完全不同。
我曾经在调试一个嵌入式 Linux 双屏设备时,发现副屏分辨率不对。查到最后,是 DRM 的 connector 和 crtc 绑定错了。你想想看,DRM 里每个屏幕是一个 connector,每个显示通道是一个 crtc。多屏显示,就是要把多个 crtc 分配给多个 connector。
调试技巧:用 modetest 命令可以列出所有 DRM 资源。我习惯先跑一遍 modetest -M <driver>,看看 connector、crtc、plane 的对应关系。这一步能排除 80% 的硬件配置问题。
3.3 双屏异显与同显原理
好,到了最实用的部分。双屏显示分两种模式:
- 同显 (Mirror/Clone):两个屏幕显示一模一样的内容
- 异显 (Extended):两个屏幕显示不同的内容,组成一个更大的桌面
咱们分别看看它们在 Android 和 Linux 下是怎么实现的。
3.3.1 Android 双屏同显
Android 原生其实不太鼓励同显。因为 SurfaceFlinger 的设计是「一个 DisplayDevice 对应一个屏幕」。要实现同显,通常有两种做法:
- 软件方案:在 App 层把画面复制一份,送到两个 DisplayDevice。缺点是性能开销大。
- 硬件方案:利用 HWC 的
DisplayDevice::makeMirror接口(Android 10+ 引入)。让 HWC 直接把一个屏幕的内容镜像到另一个屏幕。性能好,但需要硬件支持。
我记得有个项目,客户要求车载仪表盘和中控屏同显导航画面。我们用了硬件镜像,但发现副屏颜色偏绿。最后发现是 HWC 的 color transform 矩阵没配置对。嗯,这种坑防不胜防。
3.3.2 Android 双屏异显
异显是 Android 多屏的强项。从 Android 5.0 开始,系统支持 Presentation API。App 可以创建一个 Presentation 对象,指定它显示在哪个屏幕上。
关键代码逻辑:
// 获取显示管理器
DisplayManager displayManager = (DisplayManager) getSystemService(DISPLAY_SERVICE);
// 枚举所有显示设备
Display[] displays = displayManager.getDisplays();
// 选择副屏(通常索引为1)
Display secondaryDisplay = displays[1];
// 创建 Presentation
Presentation presentation = new Presentation(this, secondaryDisplay);
presentation.show();
底层原理:每个 Display 对应一个 DisplayDevice,每个 DisplayDevice 有自己的 BufferQueue 和 SurfaceFlinger 合成通道。App 通过 Presentation 提交的缓冲区,会直接送到对应的显示设备。
避坑指南:我曾经遇到一个 bug——副屏的触摸没反应。查了半天,发现是 InputDispatcher 没有把触摸事件路由到副屏的窗口。Android 的输入系统默认只把事件发给主屏。你需要用 WindowManager.LayoutParams 的 displayId 属性,显式指定窗口属于哪个屏幕。
3.3.3 Linux/Wayland 双屏同显与异显
Wayland 下,双屏配置通常由 Compositor 管理。以 Weston(Wayland 的参考实现)为例:
- 同显:Weston 支持
--clone-mode参数。启动时指定两个输出共享同一个渲染缓冲区。 - 异显:默认模式。每个输出独立渲染,客户端可以通过
wl_output接口获取屏幕信息,决定把窗口放到哪个屏幕。
底层实现上,Wayland Compositor 会为每个输出创建一个 wl_output 全局对象。客户端通过 wl_surface 和 wl_subsurface 管理窗口位置。Compositor 在合成时,根据窗口的坐标和输出区域,决定把像素送到哪个 DRM crtc。
我建议你在调试 Wayland 多屏时,用 wayland-info 工具查看输出信息。它能列出所有 wl_output 的 name、mode、scale 等参数。有一次我发现副屏的 scale 是 2,但主屏是 1,导致窗口拖过去时大小不对。这就是典型的配置不一致问题。
3.4 框架对比总结
咱们用一张表总结一下 Android 和 Linux 显示框架的差异:
| 特性 | Android (SurfaceFlinger) | Linux (Wayland) |
|---|---|---|
| 合成器 | SurfaceFlinger + HWC | Wayland Compositor (如 Weston) |
| 底层驱动 | DRM/KMS (通过 HWC 抽象) | DRM/KMS (直接调用) |
| 多屏管理 | DisplayDevice + Presentation API | wl_output + 客户端坐标管理 |
| 同显实现 | HWC mirror 或软件复制 | Compositor clone mode |
| 异显实现 | 独立 DisplayDevice + BufferQueue | 独立 wl_output + 独立渲染 |
| 调试工具 | dumpsys SurfaceFlinger, dumpsys display | wayland-info, modetest, weston-info |
说实话,这两个框架虽然上层 API 不同,但底层都绕不开 DRM/KMS。你只要把 DRM 的 connector、crtc、plane 这三个概念吃透,多屏问题就解决了一半。
个人经验:我每次接手一个新平台的多屏项目,第一件事就是跑 modetest 和 dumpsys display。把硬件拓扑和软件配置对照着看一遍。很多问题在动手写代码之前就能发现。比如某个 connector 没使能,或者某个 crtc 被占用了——这些在日志里都有明确提示。
好,这一章就到这里。下一章咱们会深入 HWC 的配置和调试,那是真正考验耐心的地方。