Appearance
平台图形栈、引擎与应用:谁把 UI 交给屏幕
返回系统栈 | 硬件与显示链 | 内核与驱动 | 跨层故障链 | 浏览器渲染管线 | Flutter 对照
这一层承接两端:上面是开发者写的状态、组件和动画;下面是内核已经提供的进程、窗口、图形设备与驱动接口。它的工作不是“调用 GPU 画一下”,而是持续解决四个问题:谁拥有窗口里的那块 buffer,什么时候可以写,什么时候能交给合成器,用户输入应该交给谁。
术语会随平台变化。Surface、swapchain、compositor、frame 是常见职责名;Win32/DWM、Wayland、Android SurfaceFlinger、macOS WindowServer 以及 Chromium Viz 的对象和跨进程边界各不相同。本文先固定职责和数据流,再放入浏览器与 Flutter 两个具体实现。
1. 总链:从开发者状态到显示器可用的 buffer
text
业务事件 / 网络结果
-> 应用状态机
-> UI 描述(DOM/CSS、Widget、原生视图)
-> 引擎的布局、绘制记录、图层树
-> 图形 API 的 command buffer
-> GPU 完成渲染到可提交的 image / surface buffer
-> 平台合成器按窗口层级合成
-> display controller 在刷新边界 scan out同一条链上有两类“完成”,不能混用:
| 完成点 | 表示什么 | 不表示什么 |
|---|---|---|
| 应用提交绘制 | 引擎已把本帧交给图形后端 | GPU 已执行、像素已出现 |
| GPU fence signal | 某批 GPU 工作已完成 | 平台合成器已经采用它 |
| present / queue buffer | 可交给窗口系统的下一帧已就绪 | 用户已经看到它 |
| display present | 显示系统在某个刷新周期采用了该 buffer | 服务端操作已成功 |
这也解释了为什么 setState、DOM mutation、requestAnimationFrame 或 Canvas.draw... 返回后,不能断言用户已经看见变化;它们最多证明链路前半段已经推进。
2. 窗口系统:输入、坐标与 surface 的所有权
应用通常不直接管理显示器。它向平台申请一个窗口或 surface,平台为窗口确定层级、坐标、缩放、裁剪、焦点、输入目标和安全边界;最终由系统合成器把多个客户端的内容组合成桌面或手机屏幕。
text
应用窗口 / web content surface
| 提交 buffer、damage、fence
v
窗口系统 / system compositor
| z-order、clip、transform、颜色与缩放
v
显示输出 surface
| present timing
v
display controller -> panel2.1 Surface 不是“一个画布对象”
它至少包含一段可交换的图像存储和一个协议:生产者何时取得空闲 buffer,写完如何 release,消费者何时采纳、何时归还。现代系统通常用多 buffer 避免 CPU/GPU 写入时显示器仍在读取同一块内存。
text
producer: acquire free image
-> render / raster
-> signal render-fence
-> queue(image, fence, damage)
consumer: wait fence only when needed
-> composite selected images
-> release old image after safe use- double/triple buffering 降低生产者与扫描输出互相等待的概率,但排队过深会增加输入到显示的延迟。
- damage region 告诉合成器哪些区域有变化;它是优化提示,不改变正确性。
- fence / semaphore 是跨 CPU、GPU、进程传递顺序的同步凭据。没有它,消费者可能采到未画完的内容,生产者也可能覆盖仍被扫描的图像。
“垂直同步”常被概括为“等下一次刷新”。准确一些说,是 producer、compositor 和 display 的提交策略围绕刷新时序协调;是否阻塞、是否丢弃过时帧、是否允许 tearing 都是平台和呈现模式的选择。
2.2 逻辑坐标、设备像素与命中测试
开发者常写 CSS pixel、dp/pt 或 Flutter logical pixel;窗口系统和引擎需要把它们映射到真实 framebuffer 像素。缩放不是最后简单乘法:文字栅格化、图片采样、裁剪边缘、输入坐标和无障碍边界都必须使用一致的 transform。
text
physical input coordinate
-> window transform / device scale
-> content coordinate
-> hit-test tree
layout coordinate
-> device scale / surface transform
-> raster pixel coordinate常见表象是“看着点中却点不到”“拖动坐标偏移”“高 DPI 下发虚”。分别先查坐标变换与 hit test、窗口 scale 变更处理、以及资源分辨率和栅格策略,不要先归因于业务事件。
2.3 平台实例
| 平台 | 系统职责 | 客户端通常交付什么 |
|---|---|---|
| Linux Wayland | compositor 同时管理窗口、输入和 buffer 协议;客户端提交 wl_buffer | 带 damage 与同步信息的 buffer |
| Windows | 应用窗口由 Win32 体系承载,桌面窗口管理器合成桌面 | DXGI/D3D surface 等图像资源,经呈现路径进入合成 |
| Android | app 与系统通过 Surface/BufferQueue 协作,SurfaceFlinger 合成显示层 | producer queue 的 graphic buffer 与 fence |
| macOS/iOS | Core Animation 维护 layer 事务并由 WindowServer / 系统显示栈合成 | CALayer 内容、Metal/OpenGL surface 或 IOSurface |
它们不是四个可互换 API;但都在分离“应用生产内容”与“系统拥有最终屏幕”。这也是普通应用无法越过系统窗口边界任意读取、绘制或截获其他应用输入的原因。
3. 图形 API 与引擎:记录工作,不直接执行像素
浏览器或 Flutter 引擎多数不会为一个圆角矩形直接调用驱动私有接口。它使用平台图形后端,例如 Vulkan、Metal、Direct3D 或 OpenGL/GLES;API 把渲染状态、资源、draw/dispatch 等编码进命令,再提交给 GPU 队列。
text
engine display list / scene
-> choose pipeline, textures, clip, transform
-> encode command buffer
-> queue submit + synchronization
-> GPU vertex/fragment/compute work
-> render target image
-> presentable surface**命令记录与 GPU 执行异步。**CPU 侧很快结束不表示 GPU 空闲;反过来,CPU 等 GPU(读回像素、复用资源过早、隐式同步)会把并行流水线重新串行化。见硬件与显示链中 GPU 队列与显存部分,以及内核与驱动中的命令验证和 fence。
3.1 Display list:为什么 UI 引擎不逐像素操作
引擎先把“画文字、裁剪、填充路径、绘制图片、透明叠加”记录成 display list 或等价绘制指令。它允许引擎在真正光栅化前做批处理、裁剪、缓存、分块与后端选择。
text
layout result
-> save / clip / transform / draw text / draw image ...
-> display list or picture
-> raster tiles or render pass这不是 DOM、Widget 或 CSS 的替代树:它只回答已知几何后“要画什么”,不负责组件身份、状态迁移或布局规则。把“组件重建”直接等同为“GPU 重画全屏”是常见误解。
3.2 图层、render pass 与离屏代价
合成图层让一部分结果可以独立变换、裁剪、滚动或调节透明度,而不必让主线程重新布局绘制全部内容。代价是 layer 元数据、纹理、显存和额外 render pass;某些效果(模糊、backdrop filter、复杂 clip)需要先把内容画到离屏目标,再作为输入采样。
因此优化不是“尽量多分层”,而是问:
- 这段内容是否高频变化,且能在不重排/重画其他内容的情况下独立合成?
- 新增 layer 会不会扩大离屏区域、纹理内存或合成次数?
- trace 是否显示问题在主线程,还是已转移到 raster/GPU?
浏览器中的 compositor layer 与 Flutter 的 Layer/RepaintBoundary 都属于这个问题域,具体触发条件不同,不能逐项照抄。对照见浏览器内核渲染细节和Flutter 对照。
4. 浏览器:多进程内容引擎如何接入平台
Web 应用代码运行在 renderer 内的 Web 执行环境。页面脚本改变 DOM/CSS 或框架状态;Blink 主线程生成布局和绘制产物;合成器、栅格与 GPU 相关职责把它们转成 compositor frame,再由 Chromium 的显示合成路径与宿主窗口系统对接。
text
JS / DOM / CSS
-> Blink main: style -> layout -> paint artifacts
-> compositor: layer tree, scroll/animation updates, tile priorities
-> raster workers / GPU resources
-> compositor frame (surfaces, render passes, quads)
-> display compositor / platform presentation4.1 为什么浏览器还要有 browser process
页面并不直接拥有操作系统窗口、网络权限或其他站点的内容。Chromium 的 browser process 负责标签、导航、权限、站点隔离和多 renderer 间的协调;renderer 负责不可信 Web 内容。这个边界既是安全边界,也影响渲染诊断:导航卡住、权限弹窗、跨 frame 输入路由、跨进程 iframe 合成,并不必然是页面主线程问题。
对 Web 开发者,最重要的落点是:
- DOM/CSS 改动是否让 Blink 需要 style/layout/paint,见浏览器渲染管线;
- JS task 是否阻塞 renderer 主线程,见浏览器运行时与帧调度;
- 内容是否已经进入 compositor,而 raster、GPU 或平台 present 才是瓶颈,应用侧需要用 trace 而不是猜测。
4.2 OOPIF 与 surface 聚合
跨站 iframe 可能在另一 renderer 进程中生成自己的 frame。父页面并不读取子页面的像素缓冲后再绘制;系统通过 surface 嵌入和合成协议组合各 frame。这保留隔离,也使嵌套内容有独立的提交节奏。开发者看到的只是 iframe;性能与安全模型里它可能已是跨进程内容生产者。
5. Flutter:framework、engine 与 embedder 的分工
Flutter 的分层通常比 Web 暴露得更直接:Dart framework 管理 Widget/Element/RenderObject,engine 管理文本、场景、栅格和图形后端,embedder 连接 Android/iOS/桌面/网页平台的窗口、输入、vsync 与 surface。
text
Dart state
-> Widget configuration
-> Element reconciliation
-> RenderObject layout / paint / semantics
-> Layer tree -> Scene
-> Flutter engine (Skia or Impeller backend)
-> embedder surface / platform compositorWidget 是配置,Element 保存身份和生命周期,RenderObject 负责几何、paint、命中测试和语义。详细对象关系与一帧调度见Flutter 对照。这里强调两个和平台承接有关的边界:
- engine 不等于 OS:engine 把 Scene 交给 embedder;后者处理原生窗口、surface 生命周期、输入事件、平台线程与 present。
- platform view 不是普通 Flutter layer:嵌入原生 View 时,系统需要把来自不同渲染体系的内容共同合成。不同平台使用不同模式,可能引入额外 copy、同步或合成限制,应在目标设备上测量。
Impeller 和 Skia 是绘制后端/策略,不是 Flutter 应用语义的一部分。渲染器切换后,某些 shader、缓存和帧时间特征会变化,但 widget 状态、Element identity 与业务正确性规则不应随之改变。
6. 开发者可控边界:写什么,验证什么
开发者不能控制显示器在哪一个具体 scanline 上取 buffer,但能决定是否把不必要的工作塞进关键路径、状态是否可追踪、资源是否有正确尺寸,以及用户看见的文案是否和事实一致。
| 你能直接设计的事 | 对链路的影响 | 不能据此推出的结论 |
|---|---|---|
| 状态机与请求 identity | 避免旧响应覆盖新状态、区分 optimistic/pending/confirmed | UI 已变不代表远端已提交 |
| 组件与对象身份 | 决定可否局部复用、正确保留焦点/滚动/动画状态 | 复用不等于零 layout 或零 raster |
| 布局与绘制范围 | 减少不必要的 layout、paint、离屏效果和资源上传 | 代码更短不等于帧更快 |
| 图片、字体、shader 预热 | 降低关键帧的解码、编译、上传峰值 | 预加载越多不一定越快或越省内存 |
| 输入与无障碍语义 | 让命中测试、焦点、读屏器与可见层一致 | 视觉正确不等于可操作 |
| 性能测量 | 将问题定位在 input/main/raster/GPU/present 某一段 | 单次本机 trace 不能代表所有设备 |
一个可复用的应用状态模板
text
idle/editing
-> submitting(requestId, localRevision)
-> confirmed(serverRevision) | rejected(error, localRevision)
late response with different requestId/revision
-> discard or reconcile; never blindly overwrite visible state这属于应用层,但它决定用户何时看到“提交中”“已保存”“需重试”。渲染栈只保证把该状态变成画面;共享事实仍需由服务端确认,不能拿 GPU fence 或一帧截图替代。
7. 实操:用一条帧追踪把边界落到证据
选一个确定交互,例如打开侧栏、拖动地图或点击提交。记录从输入到呈现的时间线,并按顺序回答:
- 输入何时到达应用?是否先等待一个长任务?
- 状态在何时改变?UI 描述是否生成了预期局部更新?
- layout、paint、raster 分别耗时多少,哪一段扩大了范围?
- 引擎是否已经提交 frame?GPU/平台合成是否在等待 buffer、纹理或 fence?
- 真正屏显的时间在哪里?不同刷新率与后台状态是否改变结果?
- 若画面写着成功,远端事实的确认事件与该帧是否有明确关联?
浏览器优先用 Chrome DevTools Performance trace、Rendering 面板和 frame 轨道;Flutter 优先用 DevTools Performance view、frame chart、raster/UI thread 事件与平台 profiler。采样前后保持同一设备、同一刷新率和相近输入路径;否则“优化”可能只是换了条件。
8. 延伸资料
- Chromium: RenderingNG architecture(访问于 2026-07-30):浏览器渲染流水线及 compositor 术语。
- Chromium: Viz documentation(访问于 2026-07-30):surface、frame sink 与显示合成架构;实现会演进。
- Android Graphics Architecture(访问于 2026-07-30):BufferQueue、SurfaceFlinger 与同步边界。
- Wayland: The Wayland Protocol(访问于 2026-07-30):客户端、compositor 与 buffer 协议。
- Flutter architectural overview(访问于 2026-07-30):framework、engine、embedder 的官方分层。
这些资料解释的是职责或某个版本的实现。把它们用于诊断时,应回到当前目标设备的 trace、frame timing 和实际配置,而不是把架构图当成运行证据。