探索 Chromium UI:Views 子系统的绘制流程和网页绘制对比
笔者最近正在爽看Chrome浏览器,我注意到UI 控件(例如地址栏、菜单、按钮)与网页内容的绘制流程是不完全相同的。所以很多内容要重新整理一下。
1. 事件循环与消息触发
Chromium 的 UI 框架在底层使用了类似于其他 UI 框架(如 Qt)那样的 事件循环 + 消息分发机制。当在 Windows 平台上收到原生消息(例如 WM_PAINT)时,这条消息会被封装并传递给 Chromium 的 UI 框架进行处理。
在 Aura 架构中,原生窗口的事件(如鼠标、键盘、绘制请求)会通过 RootWindowHost/RootWindow 流转到目标窗口。在 Views 子系统中,这意味着当控件需要重绘时,可能最终触发的就是该流程中的 WM_PAINT → 框架回调机制。
2. Window 与 Widget 抽象:由 Aura 托管跨平台窗口行为
在 Chromium 中,跨平台的窗口行为由 Aura 子系统负责,其为不同平台(Windows、Linux)封装了原生窗口(如 HWND)及事件/绘制机制。Views 框架构建于 Aura 之上,负责 UI 控件树(views::View)和 Widget。一个重要设计是:窗口/控件的功能逻辑(事件处理、控件层次、布局)与绘制逻辑分离。控件按树状结构组织,每个 View 对象负责自身 bounds、布局、子控件、绘制接口。
3. 从 Root View 开始递归绘制
当窗口收到绘制请求(例如从 native 系统触发或视图无效区标记)时,Views 框架从根控件(通常是 Widget 的 RootView)开始,依次调用绘制流程。例如,代码中的 PaintFromPaintRoot(...) 路径即属于这种“顶层→子控件”的展开。在该流程中,会通过 PaintInfo 等对象携带“绘制上下文”信息(如父控件的 bounds、录制尺寸、缩放比例)传入子控件。子控件在其 Paint() 方法中会参考这些信息来确定自己的绘制起点、变换、裁剪等。
4. views::View::Paint() 的核心逻辑
在控件树的某一节点,View::Paint(const PaintInfo& parent_paint_info) 是关键入口。主要流程包括:
- 判断控件是否处于可绘制状态(例如生命周期校验、visibility、ShouldPaint() 等)。
- 构造子控件的
PaintInfo,包括当前控件的 bounds、录制尺寸、相对于父控件的偏移、缩放类型(例如高分屏时)以及是否拥有 layer。 - 绘制缓存加速:如果当前控件的内容未改变、并且
PaintContext支持 invalid-rect 检查,那么可以跳过自身绘制,从缓存(paint_cache_)中复用之前的绘图内容。 - 若需要绘制,则设置裁剪(clip)和变换(transform)以确保控件绘制在正确的坐标系并不会溢出。裁剪基于控件 bounds 或者自定义 clip path。
- 调用
OnPaint(canvas),这是控件子类(例如Label)覆盖的方法,用来绘制自身内容(如背景、边框、文本、图像等)。 - 最后,调用
PaintChildren(paint_info),递归遍历子控件进行绘制。
5. 遍历子控件
在该控件自身内容处理完毕后,Views 框架会继续对子控件进行绘制。每个子控件被认为是一个 View,获得来自父控件传递下来的 PaintInfo,然后执行相同的 Paint() 流程。这样整个控件树被从根节点遍历为深度优先,确保每个控件都能按正确顺序绘制。
6. 与 QWidget 框架的对比:Chromium 的优化
笔者熟悉的 Qt 的 QWidget 框架中,控件重绘一般是在本地控件收到 paint 事件后直接绘制到控件的画布上,然后最终提交到底层。而在 Chromium 的 Views 框架中,有一些额外的优化和抽象层:
- Views 不总是直接将绘制命令提交到底层 surface,而是 先记录绘制命令(例如 PaintRecord)或缓存绘制结果。
- 如果控件被设置为 “layer‐backed”(即拥有
ui::Layer),其绘制命令可能被提交给 layer。Layer 层负责将这些抽象的绘制需求转化为合成器(compositor)可理解的绘制指令(例如通过方法如PaintContentsToDisplayList()来生成cc::DisplayItemList)以备后续栅格化、合成。 - 这就意味着,Views 的绘制流程并非“一次绘制→直接显示”,而是绘制命令录制/转换/合成/提交这样的多阶段流程,有利于跨平台、异步、GPU 加速的优化。
7. 提交至 GPU 渲染进程
在 layer 和 compositor 层面,绘制命令最终会被转换为 GPU 可以消费的结构。比如 cc::DisplayItemList 是一种命令列表,记录了绘制操作。然后这些命令会通过 compositor/viz 模块提交给 GPU 进程或 GPU 后端进行栅格化与显示。
“… Aura uses cc for Aura window composition, and Views uses cc through Aura for compositing different elements in the browser UI for a window.”
也就是说,在 UI 层,Views 绘制最终也可以走合成器 + GPU 路径。
关于Ui/Views和Blink浏览器网页绘制的对比
我翻阅了 Views 的文档,发现它主要负责 浏览器“本地非网页部分”的窗口控件绘制。它强调平台隔离(cross-platform)、绘制过程较为轻量,而且其实现路径是:控件通过 widget/view → layer 图层 → 最终 GPU 指令提交。
而网页渲染那部分(基于 Blink)具备 非常强大的网页绘制能力,支持精细动画、复杂控件自定义、CSS 特性等。按我的计算机图形学背景来看,它必然庞大、复杂、资源开销亦不小。
因此,从优缺点来看也很明显:
- Views 的优点在于轻量、专注于控件绘制、接近原生 UI;但缺点是其“绘制效果”的灵活性和丰富度可能略逊于网页渲染机制。
- 网页渲染(Blink)则正好相反:优点是能做出复杂、精细的效果;缺点是结构繁杂、资源吃得多、调试和优化成本高。
总的来说,二者并非竞争关系,而是各司其职:浏览器 UI 部分交由 Views 负责,而网页内容部分交由 Blink 那套机制负责。理解二者的差异,对我们在做浏览器 UI 开发或嵌入式 WebUI 时选型、优化都有帮助。
参考资料Reference
- Aura Overview – The Chromium Projects. (chromium.org)
- Graphics Architecture – Aura. (chromium.org)
- Views Overview (Chromium UI Platform) – docs. (chromium.googlesource.com)
- PaintContentsToDisplayList in Blink/cc. (chromium.googlesource.com)
- GPU Accelerated Compositing in Chrome. (chromium.org)
- Key data structures in RenderingNG (for display lists context). (Chrome for Developers)
一个捕捉的分析调用堆栈
| # | 模块名 (Module) | 函数名 (Function) | 文件名 (File) | 行号 (Line) | 摘要 / 解释 (Summary / Explanation) |
|---|---|---|---|---|---|
| 00 | ui_gfx.dll | gfx::RenderText::Draw(gfx::Canvas * canvas, bool select_all) | render_text.cc | 1050 | 实际的文本绘制操作,这是绘制 Label 内容的最终调用。 |
| 01 | ui_views.dll | views::Label::PaintText(gfx::Canvas * canvas) | label.cc | 919 | Label 控件负责绘制其文本内容的部分。 |
| 02 | ui_views.dll | views::Label::OnPaint(gfx::Canvas * canvas) | label.cc | 965 | Label 控件的自定义绘制函数,通常会调用 PaintText。 |
| 03 | ui_views.dll | views::View::Paint(const views::PaintInfo & parent_paint_info) | view.cc | 1438 | 视图 (View) 绘制自身。 |
| 04-18 | ui_views.dll | views::View::RecursivePaintHelper(...), views::View::PaintChildren(...), views::View::Paint(...) | view.cc | 多次 | 视图树的递归绘制过程。这些重复的调用表示系统正在遍历一个复杂的视图层次结构(可能有许多嵌套的 View)。 |
| 19 | ui_views.dll | views::View::PaintFromPaintRoot(const ui::PaintContext & parent_context) | view.cc | 2971 | 从绘制根节点开始绘制 View 层次结构。 |
| 20 | ui_views.dll | views::Widget::OnNativeWidgetPaint(const ui::PaintContext & context) | widget.cc | 2048 | 窗口/部件 (Widget) 接收到绘制事件,并委托其内容视图进行绘制。 |
| 21 | ui_views.dll | views::DesktopNativeWidgetAura::OnPaint(const ui::PaintContext & context) | desktop_native_widget_aura.cc | 1361 | 在桌面环境下,通过 Aura 框架处理的原生部件的绘制。 |
| 22 | ui_aura.dll | aura::Window::Paint(const ui::PaintContext & context) | window.cc | 1131 | Aura 窗口系统中的窗口绘制。 |
| 23 | ui_aura.dll | aura::Window::OnPaintLayer(const ui::PaintContext & context) | window.cc | 1539 | 窗口的绘制触发了对其关联图层的绘制。 |
| 24 | ui_compositor.dll | ui::Layer::PaintContentsToDisplayList() | layer.cc | 1552 | Compositor (合成器) 模块,将图层内容记录到显示列表中,准备进行合成。 |
| 25 | cc.dll | cc::RecordingSource::Update(...) | recording_source.cc | 81 | 合成器模块 (CC),更新记录源。 |
| 26 | cc.dll | cc::PictureLayer::Update() | picture_layer.cc | 138 | 合成器模块,更新图片图层 (PictureLayer)。 |
| 27 | cc.dll | cc::LayerTreeHost::PaintContent(...) | layer_tree_host.cc | 1702 | LayerTreeHost (图层树主机) 绘制所有需要更新内容的图层。 |
| 28 | cc.dll | cc::LayerTreeHost::DoUpdateLayers() | layer_tree_host.cc | 1015 | 执行图层更新操作。 |
| 29 | cc.dll | cc::LayerTreeHost::UpdateLayers() | layer_tree_host.cc | 893 | 启动图层更新。 |
| 30 | cc.dll | cc::SingleThreadProxy::DoPainting() | single_thread_proxy.cc | 1194 | 单线程代理 执行绘制任务。 |
| 31 | cc.dll | cc::SingleThreadProxy::BeginMainFrame(const viz::BeginFrameArgs & begin_frame_args) | single_thread_proxy.cc | 1154 | 渲染主线程 收到 BeginFrame (开始帧) 信号,开始处理新帧。 |
| 32-36 | cc.dll, base.dll | base::internal::DecayedFunctorTraits::Invoke(...), base::internal::InvokeHelper::MakeItSo(...), base::internal::Invoker::RunImpl(...), base::internal::Invoker::RunOnce(...), base::OnceCallback::Run() | bind_internal.h, callback.h | 内部 | Base 库 的函数绑定和回调机制,用于执行 BeginMainFrame。 |
| 37 | base.dll | base::TaskAnnotator::RunTaskImpl(base::PendingTask & pending_task) | task_annotator.cc | 208 | 任务执行前的准备。 |
| 38 | base.dll | base::TaskAnnotator::RunTask(...) | task_annotator.h | 105 | 任务执行的辅助函数。 |
| 39 | base.dll | base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(...) | thread_controller_with_message_pump_impl.cc | 481 | 线程控制器执行任务。 |
| 40 | base.dll | base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() | thread_controller_with_message_pump_impl.cc | 346 | 线程控制器执行工作。 |
| 41 | base.dll | base::MessagePumpForUI::DoRunLoop() | message_pump_win.cc | 260 | Windows UI 消息泵的主循环。 |
| 42 | base.dll | base::MessagePumpWin::Run(base::MessagePump::Delegate * delegate) | message_pump_win.cc | 88 | 运行 Windows 消息泵。 |
| 43 | base.dll | base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(...) | thread_controller_with_message_pump_impl.cc | 650 | 线程控制器运行循环。 |
| 44 | base.dll | base::RunLoop::Run(const base::Location & location) | run_loop.cc | 134 | 主消息循环 的核心,等待并分发任务/消息。 |
| 45 | wallpaper_demo.exe | main(int argc, char * * argv) | N/A | 187 | 程序的入口点。 |