探索 Chromium UI:Views 子系统的绘制流程和网页绘制对比

笔者最近正在爽看Chrome浏览器,我注意到UI 控件(例如地址栏、菜单、按钮)与网页内容的绘制流程是不完全相同的。所以很多内容要重新整理一下。

1. 事件循环与消息触发

Chromium 的 UI 框架在底层使用了类似于其他 UI 框架(如 Qt)那样的 事件循环 + 消息分发机制。当在 Windows 平台上收到原生消息(例如 WM_PAINT)时,这条消息会被封装并传递给 Chromium 的 UI 框架进行处理。
在 Aura 架构中,原生窗口的事件(如鼠标、键盘、绘制请求)会通过 RootWindowHostRootWindow 流转到目标窗口。在 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

一个捕捉的分析调用堆栈

#模块名 (Module)函数名 (Function)文件名 (File)行号 (Line)摘要 / 解释 (Summary / Explanation)
00ui_gfx.dllgfx::RenderText::Draw(gfx::Canvas * canvas, bool select_all)render_text.cc1050实际的文本绘制操作,这是绘制 Label 内容的最终调用。
01ui_views.dllviews::Label::PaintText(gfx::Canvas * canvas)label.cc919Label 控件负责绘制其文本内容的部分。
02ui_views.dllviews::Label::OnPaint(gfx::Canvas * canvas)label.cc965Label 控件的自定义绘制函数,通常会调用 PaintText
03ui_views.dllviews::View::Paint(const views::PaintInfo & parent_paint_info)view.cc1438视图 (View) 绘制自身。
04-18ui_views.dllviews::View::RecursivePaintHelper(...), views::View::PaintChildren(...), views::View::Paint(...)view.cc多次视图树的递归绘制过程。这些重复的调用表示系统正在遍历一个复杂的视图层次结构(可能有许多嵌套的 View)。
19ui_views.dllviews::View::PaintFromPaintRoot(const ui::PaintContext & parent_context)view.cc2971从绘制根节点开始绘制 View 层次结构。
20ui_views.dllviews::Widget::OnNativeWidgetPaint(const ui::PaintContext & context)widget.cc2048窗口/部件 (Widget) 接收到绘制事件,并委托其内容视图进行绘制。
21ui_views.dllviews::DesktopNativeWidgetAura::OnPaint(const ui::PaintContext & context)desktop_native_widget_aura.cc1361在桌面环境下,通过 Aura 框架处理的原生部件的绘制。
22ui_aura.dllaura::Window::Paint(const ui::PaintContext & context)window.cc1131Aura 窗口系统中的窗口绘制。
23ui_aura.dllaura::Window::OnPaintLayer(const ui::PaintContext & context)window.cc1539窗口的绘制触发了对其关联图层的绘制。
24ui_compositor.dllui::Layer::PaintContentsToDisplayList()layer.cc1552Compositor (合成器) 模块,将图层内容记录到显示列表中,准备进行合成。
25cc.dllcc::RecordingSource::Update(...)recording_source.cc81合成器模块 (CC),更新记录源。
26cc.dllcc::PictureLayer::Update()picture_layer.cc138合成器模块,更新图片图层 (PictureLayer)。
27cc.dllcc::LayerTreeHost::PaintContent(...)layer_tree_host.cc1702LayerTreeHost (图层树主机) 绘制所有需要更新内容的图层。
28cc.dllcc::LayerTreeHost::DoUpdateLayers()layer_tree_host.cc1015执行图层更新操作。
29cc.dllcc::LayerTreeHost::UpdateLayers()layer_tree_host.cc893启动图层更新。
30cc.dllcc::SingleThreadProxy::DoPainting()single_thread_proxy.cc1194单线程代理 执行绘制任务。
31cc.dllcc::SingleThreadProxy::BeginMainFrame(const viz::BeginFrameArgs & begin_frame_args)single_thread_proxy.cc1154渲染主线程 收到 BeginFrame (开始帧) 信号,开始处理新帧。
32-36cc.dll, base.dllbase::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
37base.dllbase::TaskAnnotator::RunTaskImpl(base::PendingTask & pending_task)task_annotator.cc208任务执行前的准备。
38base.dllbase::TaskAnnotator::RunTask(...)task_annotator.h105任务执行的辅助函数。
39base.dllbase::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(...)thread_controller_with_message_pump_impl.cc481线程控制器执行任务。
40base.dllbase::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork()thread_controller_with_message_pump_impl.cc346线程控制器执行工作。
41base.dllbase::MessagePumpForUI::DoRunLoop()message_pump_win.cc260Windows UI 消息泵的主循环。
42base.dllbase::MessagePumpWin::Run(base::MessagePump::Delegate * delegate)message_pump_win.cc88运行 Windows 消息泵。
43base.dllbase::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(...)thread_controller_with_message_pump_impl.cc650线程控制器运行循环。
44base.dllbase::RunLoop::Run(const base::Location & location)run_loop.cc134主消息循环 的核心,等待并分发任务/消息。
45wallpaper_demo.exemain(int argc, char * * argv)N/A187程序的入口点