当前位置: 首页 > news >正文

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 用 DrawQuadsRenderPass 等结构描述如何把纹理/tiles 放在屏幕上的各个位置,并包含效果/变换等。

Aggregate (Viz) 与最终显示(Display Compositor / GPU)

​ 利用 Viz 进程或 GPU process / display compositor 将来自 Render Process 的 CompositorFrame 与其他来源(例如 UI 层/Browser UI/跨域 iframe etc.)聚合(aggregate),做最终合成,然后提交 GPU 执行实际绘制与 buffer swap,最终呈现在屏幕上。

总结一下?

  1. 资源获取(Browser/Network)
    浏览器发起网络请求(HTML/CSS/JS/图片/字体等),这些资源的到达会触发后续解析与渲染。网络延迟或阻塞资源(尤其是外部 CSS/同步 JS)会直接影响渲染关键路径。
  2. HTML 解析 → DOM 构建(Renderer main thread)
    Main thread 的解析器把 HTML 变成 DOM 节点树;在解析过程中会并行发起对子资源的请求。DOM 是后续样式、布局的基础结构。
  3. CSS 解析 → CSSOM & 样式计算(Style)
    CSS 被解析成 CSSOM,主线程将 CSSOM 与 DOM 结合计算每个节点的最终计算样式(computed style)。样式变化会触发样式重算。
  4. Layout(回流 / reflow)
    根据 computed style 计算每个渲染对象的尺寸与位置(fragment tree / layout tree)。这是昂贵的步骤,某些 DOM / 样式改动会导致全树 reflow。
  5. Pre-paint / 生成 Display List(Paint)
    Blink 将布局信息转成绘制指令的中间表示(display lists / paint chunks),这些描述 “要如何绘制每个 tile/区域” —— 注意这阶段并不直接把像素写出来,而是构建绘制命令。RenderingNG 明确把这一步作为渲染流水线的一部分以便后续并行处理。
  6. Commit(主线程 → Compositor线程)
    主线程把 property trees(transform/clip/effect/scroll 等)与 display list 等提交(commit)给 compositor(rendering 的合成部分),这个提交是主线程与 compositor 线程分工的关键点。
  7. Layerize(分层)与 Property Trees 的使用
    Compositor 把 display list 拆成可合成的 layer(composited layers),并通过 property trees 管理 transform/clip/opacity/scroll 等属性。分层决定哪些内容可以独立合成、只做 GPU 合成从而避免完整 repaint。
  8. 切瓦片(Tiling)与光栅化任务分配
    每个 layer 被切成 tiles(瓦片),compositor 把需要的 tile 的 raster(光栅化)任务分配到 raster 线程或 Viz/GPU。Chrome 常用 impl-side(多线程)光栅化策略:先把 display list/send 到 raster 线程后再做 bitmap 化,避免主线程阻塞。
  9. Raster / Decode(把指令/图片解码成像素)
    Raster 线程或 GPU 把 display list + 图片解码等变为纹理(GPU texture)或像素缓存。若瓦片尚未就绪,会出现 checkerboard(格状占位)或先显示低分辨率占位再细化的策略。
  10. Activate / 构建 Compositor Frame(DrawQuads / RenderPass)
    当足够的 tiles 被 raster 完成,compositor 激活 pending tree,生成一个 Compositor Frame(由 DrawQuads、RenderPasses 描述如何把已 raster 的纹理放置与变换),并把该 frame 提交给 Viz/GPU 以进行最终聚合与绘制
  11. 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的部分我不太懂,这里真的就是图一乐层次)

模块 / 目录所在路径 /名字(大致)功能/角色简介
Blinkthird_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

非常感谢这些朋友开源的文章,让我的思考有了抓手

  1. (26 封私信 / 81 条消息) 一文看懂Chrome浏览器运行机制 - 知乎
  2. Chrome浏览器渲染机制 | 工欲善其事 必先利其器
  3. (26 封私信 / 81 条消息) 浏览器内核原理–Chromium渲染流程 - 知乎

下面是笔者参考和学习的文档,包括但不限于Chrome源码整体的架构 + 渲染部分的架构

  • 特别的,这个PPT给了我不少的启发:Life of a Pixel - Google 幻灯片
  • Blink的文档:How Blink works - Google 文档
  • 笔者的架构篇参考的谷歌文档:Multi-process Architecture
http://www.dtcms.com/a/392742.html

相关文章:

  • C# Protobuf oneof、包装器类型、枚举命名与服务支持
  • 智慧消防:科技赋能,重塑消防安全新生态
  • AI人工智能训练师五级(初级)实操模拟题
  • [数理逻辑] 决定性公理与勒贝格可测性(I) 基础知识
  • Java面向对象之多态
  • 量子计算学习续(第十五周周报)
  • Docker 入门与实践:从零开始掌握容器化技术
  • 个人用户无公网 IP 访问群晖 NAS:神卓 N600 的安全便捷方案(附踩坑经验)
  • Cpolar内网穿透实战:从零搭建远程访问服务
  • 【Python精讲 03】Python核心容器:一篇通关序列(List, Tuple)、映射(Dict)与集合(Set)
  • map_from_arrays和map_from_entries函数
  • 【EE初阶 - 网络原理】网络基本原理
  • 计算机毕设选题+技术栈选择推荐:基于Python的家教预约管理系统设计
  • 密码实现安全:形式化验证技术解析及主流工具实践
  • 并发编程的“造物主“函数——`pthread_create`
  • Python如何开发游戏
  • 新手向 算法 插入排序-yang
  • 2.0、机器学习-数据聚类与分群分析
  • 无痛c到c++
  • QTableWidget 控件入门
  • 【HarmonyOS】HMRouter配置与基本使用
  • 数据驱动下的实验设计与方差分析:从技术落地到方法论升维
  • 深度学习中的池化、线性层与激活函数
  • 【脑电分析系列】第22篇:EEG情绪识别与脑机接口(BCI)应用案例:机器学习与深度学习的实战
  • 深度学习知识点
  • 【pdf】如何将网页转换为pdf?
  • 家庭劳务智能机器人:从“科幻设想”到“推门而入”还有多远?
  • C++后台开发工具链实战
  • PortAudio--Cross-platform Open-Source Audio I/O Library
  • Oracle根据日期进行查询