Chrome View渲染机制学习小记
Chrome View渲染机制学习小记
笔者最近正在看一点Chrome的源码,觉得这里的源码非常的庞大,找到了年初看Linux源码的感觉了。好在Chrome的文档非常的齐全,可以非常好的帮助我们入门。这一篇文章更多的是将Chrome Documentations和我翻到的一些文章的一次整理和归纳。
当我们浏览器渲染界面的时候,到底发生了什么
我们都是背过八股文的,一般而言,咱们给我们的地址输入控件输入一个URL并且敲下回车之后,我们进行了多次的HTTP GET请求会话,最后拿到的是若干的HTML文件 + CSS文件 + JS文件告诉我们这个网站的内容。我们的下一步,就是将这些内容真正的显示在我们的窗口上。这个步骤就是浏览器的渲染流程。我们稍后讨论的机制也是这个流程的工作原理。(当然,这里是Chrome的,那咱们之后就直接说Chrome (View)的渲染机制了)
Parse
第一步,咱们需要知道哪些对象组件是需要被绘制的,前端的朋友都知道咱们的组件实际上被组织成了一个树(嘿!跟咱们的Qt的对象绘制好像是类似的,我们也是组织成了一个由QObject组成的树进行内存管理,QWidget组成的树确定了咱们的绘制顺序,事件通知架构),我们第一步就是将由文本构成的HTML Parse成一颗DOM树。这样,我们就将HTML初步翻译成一个可以被后端架构所更加容易绘制的网页骨干。
第二步,我们都知道现代的网页都是非常漂亮的,背后少不了CSS的功劳(虽然我写CSS实在水平稀烂,每次都只好照着别人的描摹),那么,我们只有DOM树告诉我们的网页有什么是完全不够的,我们还需要映射出来一个渲染树,告诉我们的本地浏览器如何渲染我们的网页(⭐大核心⭐),当然,就算没有CSS,咱们也有默认的样式嘛!所以这一步是少不了的,尽管你可能压根就没写CSS。
第三步,有什么了,知道如何绘制我们的控件了,下一步就是排列整齐,就像我们造好了拼图,需要补充完整成一个协调的网页一样,我们开始计算好这些部件放在哪里进行排布,paddings和大小多大,这样,我们最后构建出来了一颗布局树。
之后,我们要素齐全,我们完成了从HTML + CSS文件向一个Widget排布的绘制,这个时候我们把这个绘景交付给我们的绘制后端开始渲染和绘制。
Render Architechure
Chrome的多进程架构
碎碎念:笔者这里单独补充下Chrome的多进程架构图层,会的朋友自行跳过
我们现在的核心就是理解,现在有了可以被渲染的东西了,咱们如何将这些个东西进行渲染呢?
咱们先不论JS阻塞啊,事件通知啊等等其他的异步编程的核心内容,屏幕绘制本身就是是一个非常耗时的操作,这就要求咱们请出来一种常见的模型了——事务提交(小的说是一种事件通知)模型。这个模型咱们立马想到的就是线程池模型。我们Commit任务出去,委托给咱们的线程池进行真正的执行。这样的话,我们就实现了一种非常简单的异步模型。
Chrome的架构是类似的。不过这里不是咱们常写的小玩具(用线程),而是要综合顾虑兼容性,安全等因素(防止数据穿透,泄露等等),选择使用进程机制(嘿!这有操作系统中天然优秀的隔离机制!)作为我们的实际Worker。这样我们就很容易想出来,实际上咱们就在使用若干组的进程,一个负责Parse,提交给另一些负责了Render的进程进行渲染绘制。这就是我们的最基本的想法。
好消息是,还真是非常的相近!
这个图非常好的说明了咱们的Chrome架构的工作是做什么的。我们重点关心左侧的浏览器进程和渲染进程吧!这里的Browser Process就是我们的主进程,咱们的核心枢纽就是在这里,其他的都是子进程。主进程负责状态栏,文件,网络等。Utility进程是辅助主进程,Renderer进程就是渲染进程。插件进程不管扩展。GPU进程用来做展示部分。
当然,论权威没有人比得上咱们的Google文档的图了
这里咱们就看到了Main Process同IO控制之间是如何协作的
哦对了,这里的WebKit更加准确的说是Blink,Google 2013年fork了WebKit的一部分Components,然后做了多进程优化。这样的话更好的兼容咱们的渲染架构
具体有哪些东西咱们可以关心呢
每个渲染器进程都有一个全局的 RenderProcess 对象,用于管理与父浏览器进程的通信并维护全局状态。浏览器为每个渲染器进程维护一个对应的 RenderProcessHost 对象,用于管理浏览器状态和与渲染器的通信。浏览器和渲染器使用 Mojo 或 Chromium 的传统 IPC 系统进行通信。
每个渲染器进程都有一个或多个 RenderFrame 对象,这些对象对应于包含内容文档的框架。浏览器进程中对应的 RenderFrameHost 管理与该文档相关的状态。每个 RenderFrame 都会被赋予一个路由 ID,用于区分同一渲染器中的多个文档或框架。这些 ID 在同一个渲染器中是唯一的,但在浏览器内部则不同,因此识别一个框架需要同时拥有 RenderProcessHost 和路由 ID。浏览器与渲染器中特定文档的通信是通过这些 RenderFrameHost 对象完成的,这些对象知道如何通过 Mojo 或传统的进程间通信 (IPC) 发送消息。
在架构的文档中,我们派生了一个Client Server对,互相之间使用IPC进行通信,这里,就是咱们的浏览器——渲染进程的沟通对需要了解的部分。
正文:渲染部分
渲染本身,是笔者的薄弱点(就像笔者一直把QWidget到底如何驱动OpenGL或者Vulkan当作黑盒子处理的),下面的内容更多的是笔者的整理结论而不是我自己知道的。⚠谨慎参考⚠
大致的流程
我们将之前的东西一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。合成器线程然后栅格化每个图层。注意到咱们有一部Tiling的流程了嘛?这个部分实际上就是将我们的绘制分块(一些组件可能非常的庞大!这个时候,如果我们贸然的直接塞给一个渲染进程,这就会导致渲染工作负载不均衡)。栅格线程栅格化每一个tile并将它们存储在GPU内存中。GPU渲染在独立的进程中,不会和处理JS的进程冲突。
之后,我们通过IPC(从RenderProcessHost到RenderProcess)将合成器帧提交给浏览器进程。这时可以从UI线程添加另一个合成器帧以用于浏览器UI更改,或者从其他渲染器进程添加扩充数据。这些合成器帧被发送到GPU用来在屏幕上显示。如果发生滚动事件,合成器帧会创建另一个合成器帧并发送到GPU。
合成的好处是它可以在不涉及主线程的情况下完成。(他是直接DMA让CPU跟GPU进行交流的,玩过单片机的朋友都知道,DMA的数据传输时不会消耗CPU时间的!)合成线程不需要等待样式计算或JS执行。这就是合成动画是平滑性能的最佳选择的原因。如果需要再次计算布局或绘图,则必须涉及主线程。
Commit(主线程 → Compositor 线程)
现在我们准备贯彻事务提交模型中的提交了,我们上面说的动态布局好了Display List 和 Property Trees 是在 Blink 的主线程里生成的,但 compositor(cc 模块里的 compositor thread/impl)负责最终的合成 / 光栅 /提交 frames。为了把主线程的工作传给 compositor,需要做一次 “commit” 操作。Commit 是把所有变化过的数据(display list、layer 属性、property trees 等)原子地复制/同步给 compositor 线程使用。
此时,主线程 (Render Process 主线程) 发起 Commit 请求(通常通过某些标记,比如 SetNeedsCommit / requestAnimationFrame 等)。而Compositor 线程(同一 Render Process 内)接收这些数据。
分层
在 compositor 端,对 Display List 进行分层 (layerization),即将某些 display items / paint chunks/subtrees提升为独立的 composited layer,这样这些 layer 在接下来的阶段(raster / compositing /动画/滚动变换)中可以独立处理、复用或只更新变动部分。Property Trees 在这个阶段用来管理元素的 visual 属性(transform、clip、effect、scroll 等),决定 layer 的行为。这样的话,实际上是进一步的翻译给我们的真实绘制方便处理。
切瓦片 (Tiling) 与任务分配
layer 一确定后,为了进行高效的 raster(光栅化)与 GPU 纹理管理,需要把 layer 的内容切成若干小块 tile(瓦片)。这样在滚动/视口移动/部分内容变动时,不需要重绘整个 layer,只重绘或加载需要显示的 tiles。主要在 compositor 模块内部(Render Process 的 compositor thread + raster helper threads/worker threads),部分任务可能发往 GPU/Viz。
Raster / Decode(把指令/图片解码成像素)
在 tile 被分配以后,Raster 阶段负责把 Display List 或 Paint 指令在 tile 的区域内“播放”(play back),将 vector/绘制命令变成像素,再写入 GPU 纹理(或 CPU bitmap 后转 GPU)。同时图片/资源(image/font 等)若未解码亦在这一阶段处理。
Activate / 构建 CompositorFrame(DrawQuads / RenderPass)
现在我们准备好构建可以被提交到GPU进行绘制的东西了,就像咱们的OLED Frame一样,需要提交给屏幕进行操作,这是在在 compositor 线程/impl 层面完成。
当 pending tree 的 tile/property tree/layer 分层状态准备好(即 raster 等关键内容就绪),compositor 会 activate 它,即替换当前正在显示的 active tree,使新帧可用于显示。同时生成 Compositor Frame。这个 frame 用 DrawQuads
和 RenderPass
等结构描述如何把纹理/tiles 放在屏幕上的各个位置,并包含效果/变换等。
Aggregate (Viz) 与最终显示(Display Compositor / GPU)
利用 Viz 进程或 GPU process / display compositor 将来自 Render Process 的 CompositorFrame 与其他来源(例如 UI 层/Browser UI/跨域 iframe etc.)聚合(aggregate),做最终合成,然后提交 GPU 执行实际绘制与 buffer swap,最终呈现在屏幕上。
总结一下?
- 资源获取(Browser/Network)
浏览器发起网络请求(HTML/CSS/JS/图片/字体等),这些资源的到达会触发后续解析与渲染。网络延迟或阻塞资源(尤其是外部 CSS/同步 JS)会直接影响渲染关键路径。 - HTML 解析 → DOM 构建(Renderer main thread)
Main thread 的解析器把 HTML 变成 DOM 节点树;在解析过程中会并行发起对子资源的请求。DOM 是后续样式、布局的基础结构。 - CSS 解析 → CSSOM & 样式计算(Style)
CSS 被解析成 CSSOM,主线程将 CSSOM 与 DOM 结合计算每个节点的最终计算样式(computed style)。样式变化会触发样式重算。 - Layout(回流 / reflow)
根据 computed style 计算每个渲染对象的尺寸与位置(fragment tree / layout tree)。这是昂贵的步骤,某些 DOM / 样式改动会导致全树 reflow。 - Pre-paint / 生成 Display List(Paint)
Blink 将布局信息转成绘制指令的中间表示(display lists / paint chunks),这些描述 “要如何绘制每个 tile/区域” —— 注意这阶段并不直接把像素写出来,而是构建绘制命令。RenderingNG 明确把这一步作为渲染流水线的一部分以便后续并行处理。 - Commit(主线程 → Compositor线程)
主线程把 property trees(transform/clip/effect/scroll 等)与 display list 等提交(commit)给 compositor(rendering 的合成部分),这个提交是主线程与 compositor 线程分工的关键点。 - Layerize(分层)与 Property Trees 的使用
Compositor 把 display list 拆成可合成的 layer(composited layers),并通过 property trees 管理 transform/clip/opacity/scroll 等属性。分层决定哪些内容可以独立合成、只做 GPU 合成从而避免完整 repaint。 - 切瓦片(Tiling)与光栅化任务分配
每个 layer 被切成 tiles(瓦片),compositor 把需要的 tile 的 raster(光栅化)任务分配到 raster 线程或 Viz/GPU。Chrome 常用 impl-side(多线程)光栅化策略:先把 display list/send 到 raster 线程后再做 bitmap 化,避免主线程阻塞。 - Raster / Decode(把指令/图片解码成像素)
Raster 线程或 GPU 把 display list + 图片解码等变为纹理(GPU texture)或像素缓存。若瓦片尚未就绪,会出现 checkerboard(格状占位)或先显示低分辨率占位再细化的策略。 - Activate / 构建 Compositor Frame(DrawQuads / RenderPass)
当足够的 tiles 被 raster 完成,compositor 激活 pending tree,生成一个 Compositor Frame(由 DrawQuads、RenderPasses 描述如何把已 raster 的纹理放置与变换),并把该 frame 提交给 Viz/GPU 以进行最终聚合与绘制 - Aggregate(Viz)与显示(Display Compositor / GPU)
Viz 进程负责把来自不同 render processes(主页面、跨域 iframe、浏览器 UI)合成为一个全局的 compositor frame,再提交 GPU 做最终 draw + SwapBuffers,画面才出现在屏幕上。
[Browser Process] [Render Process] [Viz / GPU / OS 显示层]│ │ ││ 网络/导航触发 │ Blink (DOM / CSSOM / Layout / Paint / DisplayList) ││ │ → 将 DisplayList + 属性(transform/clip/effect)传入 CC ││ │ │├── UI / View 控件 <—— RenderWidgetHostView etc. —— 客户端视图层 ││ │ ││ │→ CC 模块:LayerTreeHost → commit → activate → tile/raster ││ │ │→ 将 CompositorFrame 提交给 Viz│ │ ││ │ │ viz service / display compositor 接收 frame│ │ │→ buffer swap / 在 OS Surface 上显示│ │ ││ 输入事件(scroll / touch / resize) │ Scheduler / Input handling 模块│ │ 控制动画 /滚动等是否可以在 compositor 或 property tree 中处理快路径│ │ │└──────────────────────────────────────────────────────────────────────────────┘
可能有用的源码部分
笔者翻了资料,问了问GPT,我考证了一部分内容(太多了有点,GPU的部分我不太懂,这里真的就是图一乐层次)
模块 / 目录 | 所在路径 /名字(大致) | 功能/角色简介 |
---|---|---|
Blink | third_party/blink/renderer/… | 浏览器前端引擎,负责:HTML/CSS/JS 解析 / DOM + CSSOM 构建 / Layout / Style / Paint / 流程控制(动画/scroll/transform 等) |
CC(Chrome Compositor / content collator) | cc/… | 管理 compositing 层 tree / property trees / tiles / raster / commit / activation /层之间的合成 /帧提交等,是把 Blink 的 Paint/DisplayList 变成 GPU 可用纹理 + CompositorFrame 的关键部分。对应 “layerize / raster / commit / activation” 阶段。 |
Viz(Visual / Display Compositor / GPU 展示层) | components/viz/… | 最终把多个 CompositorFrame(可能来自多个 sources/tabs/surfaces)聚合 / 做 display compositing / buffer swap / 显示到屏幕 /管理 Surface /FrameSink /host /mojom 接口等。这部分是“output”端口/平台交互部分。 |
Content / RenderWidgetHost / WebContents / View Host 类 | content/browser/… + content/renderer/… 中 RenderWidgetHostView 等类 | “View”层/客户端 UI 侧的类,负责把视图大小/输入/视图初始化/与操作系统窗口或 UI 控件结合/接受来自 compositor/viz 的帧/显示/view 重绘触发 etc. |
UI / platform / windowing / 显示后端 | ui/ 目录中与窗口/View 控件/本地的 Surface/OpenGL/Vulkan/DirectX 后端的实现 | 与操作系统交互、管理 native 窗口/Surface / 绘图上下文/硬件加速/设备像素比/缩放/window resize/屏幕显示等内容。这部分对 View 渲染影响很大,因为如果 Surface 管理不好,会拖渲染效率/清晰度/延迟。 |
Graphics / Skia / GPU 后端 | third_party/skia/… + Chromium 中 GPU 接口(command buffer / GPU service /图形后端) | 将绘制指令 / display list / paint record /纹理上传/shader 渲染等转化为 GPU 能理解的操作/将像素写到纹理或 frame buffer/处理 blend/filter/transform 等视觉效果。这是 View 渲染的“最终变像素/光栅化 /合成”阶段的核心。 |
Scheduler / Threading /任务调度 | cc/scheduler/… + Blink 层中与任务循环/动画/滚动同步的组件 + Viz 调度 | 控制什么时候启动 “BeginFrame”/commit/draw/何时响应 scroll /动画事件/如何避免主线程阻塞/如何合理分配线程任务等,对 View 渲染流畅性非常关键。 |
资源加载 /图片/字体/解码 | Blink 层中的 image decode / font loader /图像缓存 + CC 中 tile 管理 + GPU service 的纹理/资源上传 | 如果资源解码慢或纹理上传慢,就会导致 View 显示延迟或者先显示低质量 placeholder。资源管理也会成为渲染 View 的瓶颈。 |
Reference
非常感谢这些朋友开源的文章,让我的思考有了抓手
- (26 封私信 / 81 条消息) 一文看懂Chrome浏览器运行机制 - 知乎
- Chrome浏览器渲染机制 | 工欲善其事 必先利其器
- (26 封私信 / 81 条消息) 浏览器内核原理–Chromium渲染流程 - 知乎
下面是笔者参考和学习的文档,包括但不限于Chrome源码整体的架构 + 渲染部分的架构
- 特别的,这个PPT给了我不少的启发:Life of a Pixel - Google 幻灯片
- Blink的文档:How Blink works - Google 文档
- 笔者的架构篇参考的谷歌文档:Multi-process Architecture