【读书笔记】WebKit 技术内幕

本文总结下 《WebKit 技术内幕》的要点。

第一章 浏览器和浏览器内核#

浏览器#

背景:

  • 80年代后期90年代初期:世界上第一个浏览器 WorldWideWeb(后改名为 Nexus),Berners-Lee 发明,1991年公布源代码,只支持文本、简单的样式表、电影、声音和图片
  • 1993:Mosaic浏览器(后来的 Netscape 浏览器),Marc Andreessen 领导团队开发,只能显示简单的静态 HTML 元素,没有 js 和 css 。
  • 1995:IE 浏览器推出,第一次浏览器大战(网景浏览器消亡)
  • 1998:网景成立 Mozilla 基金会
  • 2003:Safari 浏览器发布
  • 2004:FireFox 浏览器1.0版本发布(开源项目),网景公司主导开发,Mozilla 基金会推动,第二次浏览器大战
  • 2005:苹果公司释放 Safari 浏览器中非常重要部件源码,发起开源项目 WebKit (Safari 浏览器内核),推出 Safari 浏览器移动版
  • 2008:Chromium 被创建,以苹果开源项目 WebKit 作为内核,目标是创建一个快速、支持众多操作系统的浏览器
  • 2009:Chrome 发布,以 Chromium 稳定版本作为基础

浏览器功能包括:网络、资源管理、网页浏览、多页面管理、插件和扩展、书签管理、历史记录管理、设置管理、下载管理、账户和同步、安全机制、隐私管理、外观主题、开发者工具等。

HTML5类别:10个大类别,每个类别由众多技术或者规范组成。

  • 离线:Application cache、Local Storage、Index DB、在线/离线事件
  • 存储:Application cache、Local Storage、Index DB 等
  • 连接:Web Sockets、Server-sent 事件
  • 文件访问:File API、File System、File Writer、ProgressEvents
  • 语义:各种新的元素,包括 Media、structural、国际化、Link relation、属性、form 类型、microdata 等方面
  • 音频和视频:HTML5 Video、Web Audio、WebRTC、Video track 等
  • 3D和图形:Canvas 2D、3D CSS 变换、WebGL、SVG等
  • 展示:CSS 2D/3D变换、转换(transition)、WebFonts等
  • 性能:Web Worker、HTTP caching等
  • 其他:触控和鼠标、Shadow DOM、CSS masking 等

JavaScript 语言是 EMCAScript 规范的一种实现,最初用于网页的脚本语言还有 JScript,标准化组织制定脚本语言的规范 EMCAScript 。

用户代理 User Agent 作用是表明浏览器的身份,网页内容提供商可以根据用户代理为不同的浏览器发送不同的网页内容。用户代理字符串越来越长:某种浏览器的流行,很多内容提供商和网站需要根据流行的浏览器来定制内容,比如说 IE 发现很多内容提供商传给 IE 浏览器的内容没有传给火狐的丰富(虽然 IE 也能支持他们),IE 就将其用户代理添加 "Mozilla" 等相关字符串,表明这是一个可以和 Mozilla 兼容的 IE 浏览器(内容提供商会根据这个 "Mozilla" 字符串将发送给 Firefox 浏览器的呢绒也发送给 IE 浏览器,因为在他们看来这些都是 "Mozilla" 的浏览器)。

浏览器内核及特性#

浏览器内核(渲染引擎):主要的作用是将页面转化为可视化(准确讲还要加上可听化)的图像结果。渲染就是更具描述或者定义构建数学模型,通过模拟生成图像的过程。浏览器渲染引擎就是能够将 HTML/CSS/JavaScript 文本及其相应的资源文件转换成图像记过的模块。

Trident Gecko WebKit
基于渲染引擎的浏览器或者 Web 平台 IE Firefox Safari,Chromium/Chrome,Android浏览器,ChromeOS,WebOS等

浏览器渲染引擎功能模块:

渲染引擎模块及其依赖的模块
  • HTML 解释器:解释 HTML 文本,将 HTML 文本解释成 DOM 树。
  • CSS 解释器:级联样式表的解释器,为 DOM 中的各个元素对象计算出样式信息。
  • 布局:DOM 创建之后,WebKit 需要将其中的元素对象同样式信息结合起来,计算它们的大小位置等布局信息,形成一个能够表示这所有信息的内部表示模型。
  • JavaScript 引擎:JavaScript 引擎能够解释 JavaScript 代码并通过 DOM 接口和 CSSOM 接口来修改网页内容和样式信息,改变渲染结果。
  • 绘图:使用图形库将布局计算后的各个网页的节点绘制成图像结果。

渲染引擎的一般渲染过程:实线表示先后关系,虚线表示在渲染过程中每个阶段可能使用到的其他模块

渲染引擎的一般渲染过程及各阶段依赖的其他模块

WebKit 内核#

广义 WebKit :WebKit 项目

狭义 WebKit:WebKit 嵌入式接口,即在 WebCore (包含 HTML 解释器、CSS 解释器和布局等模块)和 JavaScript 引擎之上的一层绑定和嵌入式编程接口,可以被各种浏览器调用。

WebKit项目的大模块

WebKit 移植(Port):由于各自需求不同,或者操作系统不同,或者依赖的模块不同(如 2D 图形库,有 CG、skia、cairo、Qt等),操作系统的开发者必然需要 WebKit 设计和定义一套灵活的框架系统,不同厂商基于框架系统完成基于自身操作系统和依赖模块的实现。WebKit 每个移植的实现都不同,对 HTML5 规范的支持也不尽相同。

WebKit2:一组支持新架构的全新绑定和接口层,该接口和调用者代码与网页渲染工作代码不在同一个进程。

Blink:Chromium 内核,2013年4月从 WebKit 复制出来并独立运作。


第二章 HTML 网页和结构#

网页构成#

基本元素和树状结构

网页结构#

当前网页嵌入新的框结构:frameset、frame、iframe

最简单的框结构就是单一的框,其文档中没有包含任何其他的框

多框结构网页对于移动领域不适合(对触控操作是一场灾难)

层次结构#

对于框结构内部的文档机构

网页的元素可能分布在不同的层次,某些元素可能不同于它的父元素所在层次(某些情况下 WebKit 需要为该元素和它的子女建立一个新层)。对于需要复杂变换和处理的元素(如video、3D变换、复杂的2D3D绘图操作),它们需要新层从而实现渲染引擎在处理上的方便或高效。

根层:图中最大的层,网页构建层次结构首先构件根节点时创建的层,对应着整个网页文档对象。

WebKit 网页渲染过程#

浏览器主要作用:将用户输入的 URL 转变成可视化图像。包含两个过程:

  • 网页加载过程,从 URL 到 DOM 树
  • 网页渲染过程,从 DOM 树到可视化图像

视图:viewport,当前可见区域,一般网页都要比屏幕可视面积大

网页 URL 到构建 DOM 树:#

urlToDOM
  1. 用户输入 URL ,WebKit 调用其资源加载器加载该 URL 对应的网页
  2. 加载器依赖网络模块建立连接,发送请求并接收答复
  3. WebKit 接收到各种网页或者资源的数据,其中某些资源可能是同步或异步获取
  4. 网页被交给 HTML 解释器转变成一系列词语(Token)
  5. 解释器根据词语构建节点(Node),形成 DOM 树
  6. 如果节点是 JavaScript 代码,调用 JavaScript 引擎解释并执行
  7. JavaScript 代码可能会修改 DOM 树的结构
  8. 如果节点需要依赖其他资源,例如图片、CSS、视频等,调用资源加载器来加载它们(异步加载,不会阻碍当前 DOM 树的继续创建);如果是 JavaScript 资源 URL (并且没有标明以异步方式),需要停止当前 DOM 树的创建知道 JavaScript 资源加载并被 JavaScript 引擎执行后才继续 DOM 树的创建。

上述过程中网页会有两个重要事件:DOMContentLoaded (原文中为 DOMConent)与 load 事件

  • DOMContentLoaded:初始 HTML 文档被完全加载和解析完成之后即触发,无需等待样式表、图像、子框架 subframe 的完成加载(这里需要等待的只是会阻碍 DOM 树创建完成的元素加载,包括 script 标签),参考 MDN
  • Load:DOM 树构建完并且网页所依赖的资源都加载完之后发生

利用 CSS 和 DOM 树构建 RenderObject 树直到绘图上下文:#

css-dom-renderObject
  1. CSS 文件被 CSS 解释器解释成内部表示结构
  2. CSS 解释器工作完之后,在 DOM 树上附加解释后的样式信息,即为 RenderObject 树
  3. RenderObject 节点在创建的同时,WebKit 会根据网页的层级结构创建 RenderLayer 树,同时构建一个虚拟的绘图上下文

CSSOM,DOM,RenderObject 树,RenderLayer 树会一直存在直到网页销毁

绘图上下文到最终的图像:#

绘图上下文到图像
  • 绘图上下文:与平台无关的抽象类,将每个绘图操作桥接到不同的具体实现类(绘图具体实现类)
  • 绘图实现类:将 2D 图形库与 3D 图形库绘制结果保存下来,交给浏览器来同浏览器界面一起显示。

第三章 WebKit 架构和模块#

WebKit 架构#

webkit 架构

虚线部分代表该模块在不同浏览器使用的 WebKit 内核中的实现是不一样的(不是普遍共享),实线框标记的模块表示基本上是共享的。

Chromium 浏览器结构#

基于 WebKit (Blink)

chromium module
  • GPU/Command Buffer ,硬件加速架构
  • Content 模块、Content 接口(API):Chromium 对渲染网页功能的抽象,将下面的渲染、安全、插件机制隐藏起来,提供一个接口层(供上层模块或其他项目使用)。也可以在 WebKit 的 Chromium 移植上渲染网页内容(无法获取 Content 层中实现的沙箱模型、跨进程的 GPU 硬件加速机制等)
  • Content Shell:使用 Content API 来包装的一层简单的壳,用户可以使用 Content 模块来渲染和显示网页内容。作用可以用来测试 Content 模块很多功能的正确性,也可以作为一种外部项目开发的参考
  • Android WebView:利用 Chromium 实现来替换原来 Android 默认的 WebView

多进程模型:#

chromium-multiprocess

上图针对桌面版的 Chromium, NPAPI 与 Pepper 均为插件。对于 Android 版,主体进程模型大致相同,GPU 进程演变成 Browser 进程的一个线程(节省资源),Render 进程演变为服务(service)进程,数目会受到限制,设计到影子(Phantom)标签(浏览器会将后台的网页所使用的渲染设施清楚,只是原来的一个影子,用户再次切换回的时候网页需要重新加载和渲染)。

  • Browser 进程:浏览器主进程,负责浏览器界面的显示、各个页面的管理,是所有其他类型进程的祖先,负责它们的创建和销毁等工作,有且仅有一个。
  • Renderer 进程:网页的渲染进程,负责页面的渲染工作,Blink/WebKit 的渲染工作主要在这个进程中完成,Renderer 进程数量不一定与用户打开页面的数量一致:
    • process-per-site-instance:每一个页面都创建一个独立的 Renderer 进程
    • process-per-site:属于同一个域的页面共享同一个进程
    • process-per-tab:每个标签页都创建一个独立进程(Chromium 默认行为)
    • single process:不为页面创建任何独立的进程,所有渲染工作都在 Browser 进程中的多个线程中进行
  • NPAPI 插件进程:该进程是为 NPAPI 类型的插件而创建,原则是每种类型的插件只会创建一次,而且仅当使用时才被创建,有多个网页需要使用同一类型的插件的时候,插件进程会为每个使用者创建一个实例,即插件进程是被共享的。
  • GPU 进程:最多只有一个,当且仅当 GPU 硬件加速打开的时候才会被创建,主要用于对 3D 图形加速调用实现。
  • Pepper 插件进程: 同 NPAPI 插件进程,为 Pepper 插件创建。
  • 其他类型的进程:如 Linux 下的 Zygote 进程(创建 Renderer 进程)、Sandbox 准备进程(安全机制)

Browser 进程和 Renderer 进程都是在 WebKit 接口之外由 Chromium 引入。

WebKit接口层到用户界面的路径
  • WebKit 黏附层:Chromium 与 WebKit 一些类型不一致的桥接
  • Renderer :进程间通信,接收 Browser 请求,调用相应的 WebKit 接口层并将 WebKit 的结果发送回去
  • Browser 进程:RendererHost,给 Renderer 进程发送请求并接收来自 Renderer 进程的结果。
  • Contents :网页中可能有多个需要绘制的内容

第四章 资源加载和网络栈#

WebKit 资源加载机制#

资源缓存:#

基本思想是建立一个资源的缓存池,当 WebKit 需要请求资源时,先从资源池中查找是否存在相应的资源(根据资源的 URL 标识不同资源),如果有则取出以便使用,没有就创建一个新的 CachedResource 子类对象,发送真正的请求给服务器,WebKit 收到资源后将其设置到该资源类的对象中去,以便于缓存(这里的缓存指内存缓存)后下次使用。

资源池中的资源生命周期:LRU 算法(Least Recent Used 最近最少使用)

对于判断下次使用的时候是否需要更新该资源(服务器可能在某段时间之后更新了该资源):WebKit 的做法是首先判断资源是否在资源池中,如果是则发送一个 HTTP 请求给服务器说明该资源在本地的一些信息,服务器更具该信息作判断,如果没有更新则发送回状态码 304(缓存命中)表明无需更新,直接利用资源池中原来的资源,否则 WebKit 申请下载最新的资源内容(这里可以参考 http权威指南 中的缓存模块)。可以通过开发者工具中的 network 打开或关闭此机制,实现的原理就是直接清除 MemoryCache 对象中的所有资源(全局唯一),清除该资源后就会重新打开缓存机制,这样就会在资源池中找不到所要请求的资源,请求报文中就不会有条件首部字段。

资源加载器#

  • 针对每种资源类型的特定加载器:仅加载某一种资源,属于它的调用者
  • 资源缓存机制的资源加载器:所有特定加载器都共享它来查找并插入缓存资源,CachedResourceLoader 类,属于 HTML 文档对象
  • 通用资源加载器:ResourceLoader 类,在 WebKit 需要从网络或者文件系统获取资源的时候使用 ResourceLoader 类只负责获得资源的数据,属于 CachedResource 类。

加载过程:#

通常一些资源的加载时异步的(如图片、css文件),其获取和加载不会阻碍当前 WebKit 的渲染过程。

但特殊资源如 JavaScript 代码文件的加载会阻碍主线程的渲染过程,这会严重影响 WebKit 下载资源的效率,后面可能还有许多需要下载的资源。 WebKit 的做法是:当前主线程被阻碍,启动另外一个线程去遍历后面的 HTML 网页,收集需要的资源的 URL,然后发送请求,同时能够并发下载这些资源(甚至 JavaScript 代码资源)。

资源加载过程

Chromium 多进程资源加载#

在图 4-6 中 ResourceHandle 类之下的部分是不同移植对获取资源的不同实现,Chromium 中是 多进程资源加载 。

Chromium的多进程资源加载

Renderer 进程在网页的加载过程中需要获取资源,但由于安全性(沙箱模型打开后 Renderer 进程没有权限获取资源)和效率(资源共享)的考虑,Renderer 进程的资源获取实际上是通过进程间的通信将任务交给 Browser 进程来完成,Browser 进程有权限从网络或者本地获取资源。

网络栈#

要实现一些基础的部分,如 HTTP 协议、DNS 解析等模块,还包括一些为了减少网络时间而引入的新技术(如 SPDY、QUIC 等)。

域名解析:为了保持效率通常会保存解析后的域名。

磁盘本地缓存:#

缓存通常是一个表,对于整个表的操作作用在 Backend 类上,包括创建表中的一个个项,每个项由关键字来唯一个确定,这个关键字就是资源的 URL。对于项目内的操作包括读写都是由 Entry 类来处理。

  • Backend 类表示整个磁盘缓存,是所有磁盘缓存操作的主入口,表示一个缓存表
  • Entry 类指的是表中的表项,表项的结构分为两个部分
    • 第一部分用于标记自己,包括各种元数据信息和自身的内容,较少变动
    • 第二部分主要为表项的回收算法服务,大小固定,经常发生变动,里面保存了回收算法所需要的信息

表和表项如何组织和存储在磁盘上:至少有一个索引文件和四个数据文件(块文件),每个块文件的大小是固定的,当资源文件超过某个块的大小就会为其分配多个块来解决,但最多不能超过四个块,超过四个块能存储的时候会建立单独的文件来保存。如果一个表项需要分配四个块则这些块在文件中的索引位置是对齐的(起始块的位置是4 的倍数)

  • 索引文件:用来检索存放在数据文件中的众多索引项,用来索引表项,包括一个索引头部和索引地址表。直接将文件映射到内存地址。
    • 头部用来表示该索引文件的信息,如索引文件版本号、索引项数量、文件大小等。
    • 索引地址表:保存各个表项对应的索引地址
  • 数据文件:块文件,有很多不同大小的块,用于快速检索,这些数据块的内容是表项,包括 HTTP 文件头、请求数据和资源数据。
使用索引文件和数据文件的表项索引方式

安全机制#

支持一种新的标准 HSTS 协议(HTTP Strict Transport Security):能够让网络服务器声明它只支持 HTTPS 协议。浏览器能够理解服务器的声明,发送基于 HTTPS 的连接和请求。因为通常情况下用户不会输入方案 scheme ,浏览器的补齐功能通常会加入 scheme 。

高性能网络栈#

DNS 预取和 TCP 预连接:

  • DNS 预取就是利用现有的 DNS 机制,提前解析网页中可能的网络连接。当用户正在浏览当前网页的时候,Chromium 提取网页中的超链接,将域名抽取出来,利用比较少的 CPU 和网络带宽来解析这些域名和 IP 地址,直接利用系统的域名解析机制,不会阻碍当前网络栈的工作,同时用户不会觉察到这一过程。DNS 预解析不仅应用于网页中的超链接,当用户在地址栏中输入地址后,候选项同输入地址很匹配的时候,在用户敲下回车键获取网页之前,Chromium 就已经开始用 DNS 预解析技术解析该域名。
  • TCP 预连接同 DNS 预取一样,即在预测到用户即将连接的网站(网页中的超链接、地址栏中的输入地址相匹配的候选项),在用户点击或敲下回车键之前开始尝试 TCP 连接。通常使用追踪技术或者其他一些启发式规则、暗示来预测用户的期望地址。

支持 HTTP 管线化技术(Pipelining):

  • HTTP 1.1 中新增的管线化技术:将多个 HTTP 请求一次性提交给服务器,可能将多个 HTTP 请求填充在 TCP 数据包内(相当于 HTTP 权威指南中串行事务处理中的管道化连接),需要使用持久连接,并且只有 GET 和 HEAD 等幂等请求可以进行管线化。

  • SPDY:解决 HTTP 管线技术的使用限制。SPDY 协议是一种新的会话层协议,定义在 HTTP 协议和 TCP 协议之间。核心思想是多路复用,仅使用一个连接来传输一个网页中的众多资源,没有改变 HTTP 协议,将 HTTP 协议头通过 SPDY 来封装和传输,服务器只需要从 SPDY 的消息头中获取各个资源的 HTTP 头即可。SPDY 的工作方式有以下四个特征:

    • 利用一个 TCP 连接来传输不限个数的资源请求的读写数据流。
    • 根据资源请求的特性和优先级, SPDY 可以调整这些资源请求的优先级。
    • 只对 https 请求使用压缩技术
    • 当用户需要浏览某个网页,支持 SPDY 的服务器在发送网页内容时可以发送一些信息给浏览器告诉后面可能需要哪些资源,浏览器可以提前知道并决定是否需要下载。
    SPDY 协议所处的层次
  • QUIC:新的网络传输协议,改进 UDP 数据协议的能力。


第五章 HTML 解释器和 DOM 模型#

DOM 模型#

DOM 以面向对象的方式来描述文档(HTML 文档、XML 文档、XHTML 文档)。DOM 定义的是一组与平台、语言无关的接口,该接口允许编程语言动态访问和更改结构化文档。W3C 标准化组织已经定义了 DOM Level 1(1998)、DOM Level 2(2000)、DOM Level 3(2004)、DOM Level 4 等 DOM 接口标准。

DOM 结构构成的基本要素是节点:文档节点(整个文档 Document)、元素节点(HTML 中的标记 Tag)、属性节点(标记的属性)、Entity 节点、ProcessingInstruction 节点、CDataSection 节点、注释(Comment)节点。

HTML 解释器#

HTML 解释器的工作就是将从网络或者本地磁盘获取的 HTML 网页和资源从字节流解释成 DOM 树结构。

从字节流到DOM树

WebKit 构建 DOM 树需要使用的主要类:

WebKit 构建 DOM 所使用的主要基础设施类
  • HTMLDocument:文档,继承自 Document(有两个子类,另一个是 XMLDocument),DOM 树的根节点

  • Frame:框,框内包含文档。这里没有描述内嵌的复杂框结构,实际应用中网页内嵌框会重复这样的动作。

  • FrameLoader:框中内容加载器,类似资源和资源的加载器。

  • DocumentLoader:帮助加载 HTML 文档并从字节流到构建的 DOM 树。

  • DocumentWriter:创建 DOM 树的根节点 HTMLDocument 对象。包含两个成员变量:

    • HTML 解释器 HTMLDocumentParser 类
    • 用于文档的字符解码的类
  • HTMLDocumentParser:管理类,包括各种工具,如字符串到词语需要词法分析器 HTMLTokenizer、XSSAuditor 安全检查(验证词语流)。

  • HTMLTreeBuilder:负责创建 DOM 树,能通过词语创建一个个的节点对象。

  • HTMLConstructionSite:将 HTMLTreeBuilder 创建的节点对象构建成 DOM 树,包括为元素节点创建属性节点。

  • HTMLTokenizer :词法分析,输入字符串,输出一个个词语。

词法分析:词法分析之前,解释器首先检查网页内容使用的编码格式以使用合适的解码器,将字节流转化为特定格式的字符串。字节流可能分段,输入的字符串可能也是分段,但词法分析器会维护自己内部的状态信息,每次都会根据上次设置的内部状态和上次处理之后的字符串来生成一个新的词语。

节点到 DOM 树:HTML 文档的 Tag 标签有开始和结束标记,故构建这一过程可以使用栈结构。

WebKit 中用来表示 DOM 结构的相关类:

  • 一切的基础都是 Node 类(同 DOM 标准一样)
  • Node 类继承自 EventTarget 类,表明 Node 类能够接受事件
WebKit的节点类

DOM 事件机制#

基于 WebKit 的浏览器事件处理过程:首先检测事件发生处的元素有无监听者,如果网页的相关节点注册了事件的监听者则浏览器会将事件派发给 WebKit 内核来处理。另外浏览器可能也需要处理这样的事件(浏览器对于有些事件必须响应从而做出默认处理,比如通过鼠标滚轮来翻滚网页,鼠标所在位置的 HTML 元素上注册了滚动事件监听器)。事件到达 WebKit 内核即渲染引擎接收到一个事件后,会先检查那个元素是直接的事件目标,然后会经过自顶向下和自底向上的过程。

Shadow DOM#

封装一些内部节点,使其中的内部 DOM 节点仅在特定范围内可见。如我们使用的 video、audio 标签就类似是一种 Shadow DOM。

Shadow DOM 的事件处理:由于 Shadow DOM 的子树在整个网页的 DOM 树中不可见 ,事件处理中的事件目标就是包含 Shadow DOM 子树的节点对象,事件捕获在 Shadow DOM 子树中会继续传递,Shadow DOM 子树中的事件冒泡会同时向整个文档的 DOM 上传递该事件。


第六章 CSS 解释器和样式布局#

基础#

CSS 解释器和规则匹配处于 DOM 树建立之后,RenderObject 树建立之前。

样式规则:CSS 规范中最基本的组成。一个规则包括两个部分,规则头和规则体。规则头由一个或多个选择器组成,规则体则由一个或者样式多个声明组成,每个样式声明由样式名和样式值构成。

选择器:选择器描述得越具体优先级越高。

框模型:HTML 网页中每个可视元素的布局都是依靠框模型(盒模型)来设计的,可视是因为有很多 HTML 元素不是用来显示(如用来表示语义的元素)。

包含块:框模型就是在包含块中进行计算和确定的,即计算元素的箱子位置和大小是相对于包含块进行计算的。

  • 根元素的包含块称为初始包含块,通常大小为可视区域。
  • position 为 static(默认)或者 relative 的元素,其包含块为最近祖先的箱子模型中的内容区域(content)
  • position 为 fixed,该元素的包含块脱离了 HTML 文档,固定在可视区域的某个特定位置。
  • position 为 absolute,该元素的包含块由最近的含有属性 absolute 、relative、fixed 祖先元素决定。
    • 该元素具有 display 为 inline,包含块由包含该元素祖先中的第一个和最后一个 inline 框的内边距的区域。
    • 该元素 display 不为 inline,包含块则是该祖先的内边距所包围的区域。

DOM 与 CSSOM:DOM 提供了接口让 JavaScript 修改 HTML 文档,CSSOM 提供了接口让 JavaScript 获得和修改 CSS 代码设置的样式信息。

CSS 解释器和规则匹配#

样式规则是解释器的输出结构,是样式匹配的输入数据。CSS 解释过程是指从 CSS 字符串经过 CSS 解释器处理后变成渲染引擎的内部规则表示的过程。如WebKit 的内部结构表示就是将 CSS 样式规则使用一些特殊的类表示。

样式规则匹配:根据元素的信息(如标签名、类别等),从样式规则中查找最匹配的规则,然后将样式信息保存到RenderStyle 对象中,RenderStyle 对象被 RenderObject 类所管理和使用。

WebKit 布局#

RenderObject 对象创建后并不知道自己的位置、大小等,需要根据框模型来计算它们的位置、大小等信息(布局计算)。

布局计算:

  • 布局计算有两类:
    • 对整个 RenderObject 树进行计算
    • 对 RenderObject 树中某个子树的计算,如文本元素或者 overflow:auto 块的计算
  • 布局计算是一个递归的过程:一个节点的大小通常需要先计算它的子节点的位置、大小等。首先需要判断是否需要重新计算,通常根据检查相应的标记位、子女节点是否需要计算布局等,然后确定网页的宽度和垂直方向上的外边距。遍历每一个子女节点,依次计算它们的布局,对于元素的大小,除非网页定义了页面元素的宽高,一般来说页面元素的宽高是在布局的时候通过计算的出来的(如果元素有子女则递归这一过程),节点的高度取决于子女节点的高度。
  • 需要重新计算布局:样式发生变化,都需要重新计算。
    • 可视区域(viewport)变化都需要重新计算布局(网页的包含块大小发生了变化),例如网页首次被打开需要设置网页的可视区域(计算布局)
    • 网页的动画会触发重新计算布局,网页显示结束后,动画可能改变样式属性。
    • JavaScript 代码通过 CSSDOM 等直接修改样式信息会重新计算布局。
    • 用户的交互如翻滚网页也会重新计算布局。

第七章 渲染基础#

RenderObject 树和其他树(如 RenderLayer 树等)构成了 WebKit 渲染的主要基础设施。

RenderObject 树#

RenderObject 树是基于 DOM 树建立起来的一棵新树,是为了布局计算和渲染等机制而构建的一种新的内部表示。RenderObject 树与 DOM 树不是一一对应关系。RenderObject 对象创建的规则:

  • DOM 树的 document 节点
  • DOM 树中的可视节点(html、body、div 等),不会为非可视节点创建 RenderObject 对象(meta、script、head 等)
  • 匿名 RenderObject 节点,不对应于 DOM 树中的任何节点,为了 WebKit 处理上的需要(如匿名 RenderBlock 节点)

网页层次和 RenderLayer 树#

网页分层的原因:

  • 方便网页开发者开发网页并设置网页的层次
  • 为了 WebKit 处理上的便利即为了简化渲染的逻辑

RenderLayer 树基于 RenderObject 树建立起来,RenderLayer 节点与 RenderObject 节点是一对多,一般来说某个 RenderObject 节点的后代都属于该节点,除非 WebKit 根据规则为某个后代 RenderObject 节点创建了一个新的 RenderLayer 对象。哪些情况下的 RenderObject 节点需要建立新的 RenderLayer 节点:

  • DOM 树的 Document 节点对应 RenderView 节点
  • DOM 树中的 Document 的子女节点(HTML 节点)对应 RenderBlock 节点
  • 显示指定 CSS 位置的 RenderObject 节点
  • 有透明效果的 RenderObject 节点
  • 节点有溢出 ( overflow )、alpha 或者反射等效果的 RenderObject 节点
  • 使用 Canvas 2D 和 3D( WebGL )技术的 RenderObject 节点
  • Video 节点对应的 RenderObject 节点

RederLayer 树:一个 RenderLayer 节点的父亲就是该 RenderLayer 节点对应的 RenderObject 节点的祖先链中最近的祖先,并且祖先所在的 RenderLayer 节点同该节点的 RenderLayer 节点不同(除根节点)。每个 RenderLayer 节点包含的 RenderObject 节点其实是一棵 RenderObject 子树。

示例代码 RenderObject,RenderLayer

需要注意的是第二个 layer 层中包含一个匿名(Anonymous)的 RenderBlock 节点,该匿名节点包含了 RenderText 和 RenderInline 等子节点。可以看出这个匿名的 RenderBlock 节点下对应的 DOM 节点都是行内元素(如 a 、img、input、select),这里是为了 WebKit 处理上的需要。另外需要注意的是第一二个 layer 层是在创建 DOM 树之后创建的,而第三个 RenderLayer 对象是在 WebKit 执行 JavaScript 代码时才被创建的。

渲染方式#

在 WebKit 所有绘图操作都是在绘图上下文中来进行。绘图上下文可以分为两种类型:

  • 2D 绘图上下文:提供基本绘图单元的绘制接口(包括画点、线、图片、多边形、文字等)以及设置绘图的样式(颜色、线宽、字号大小、渐变等)。平台实现既可以使用 CPU 来完成 2D 相关的操作,也可以使用 3D 图形接口(OpenGL)来完成。
  • 3D 绘图上下文:支持 CSS3D、WebGL 等。因为性能问题,WebKit 的移植通常都是使用 3D 图形接口(如 OpenGL 或者 Direct3D 等技术)来实现。

绘图操作:每个 RenderLayer 对象都可以想象成图像中的一个层,各个层一同构成了一个图像,每个层对应网页中的一个或者一些可视元素,将这些元素绘制内容到该层上的过程即为绘图操作。

  • 软件绘图:绘图操作使用 CPU 完成
  • GPU 硬件加速绘图:绘图操作使用 GPU 完成
  • 合成化渲染:使用了合成技术。理想情况下每个层都有绘制的存储区域用于保存绘图的结果,最后需要将这些层的内容合并到同一个图像中,这个过程即为合成。

对于常见的 2D 绘图操作,使用 GPU 绘图不一定比使用 CPU 绘图在性能上有优势,例如绘制文字、点、线等,CPU 的使用缓存机制有效减少了重复绘制的开销而且不需要 GPU 的并行性,其次 GPU 的内存资源相对于 CPU 的内存资源来说比较紧张,而且网页的分层使得 GPU 的内存使用相对比较多。???

渲染方式:

  • 软件渲染:使用 CPU 来渲染每层的内容,没有合成阶段,软件渲染中的结果就是一个位图(Bitmap,实际上就是一块 CPU 使用的内存空间),绘制每一层的时候都使用该位图,区别在于绘制的位置可能不一样。

    软件渲染只能处理 2D 方面的操作,适用于简单的没有复杂绘图或者多媒体方面需求的网页,对于 HTML5 因为能力不足(如 CSS3D、WebGL)或者性能不好(如视频、Canvas 2D)而不适用。软件渲染同硬件加速渲染的另一个不同地方就是对于更新区域的处理:网页中有一个更新小型区域(如动画)的请求时,软件渲染可能只需要计算一个极小的区域,硬件渲染可能需要重新绘制其中一层或者多层然后再合成这些层(代价要大得多)。

  • 硬件加速渲染

  • 混合模式:

    • 对于硬件加速的合成化渲染方式:每个层的绘制和所有层的合成均使用 GPU 硬件来完成。虽然支持硬件加速机制会消耗更多的内存资源,但:
      • 硬件加速机制能够支持现在所有的 HTML5 定义的 2D 或者 3D 绘图标准
      • 如果需要更新某个层的一个区域,因为软件渲染没有为一层提供后端存储,因而它需要将和这个区域有重叠部分的所有层次相关区域以此从后向前重新绘制一遍,而硬件加速渲染只需要重新绘制更新发生的层次,某些情况下软件渲染的代价更大(取决于网页的结构和渲染策略)。
    • 软件绘图的合成化渲染方式:使用 CPU 绘图方式来绘制某些层,使用 GPU 来绘制其他一些层,因为很多网页可能既包含基本 HTML 元素,也包含一些 HTML5 新功能。

WebKit 软件渲染过程#

  • WebKit 如何遍历 RenderLayer 树来绘制各个层:对于每个 RenderObject 对象,分为三个阶段绘制自己

    1. 绘制该层中所有块的背景和边框
    2. 绘制浮动内容
    3. 前景,即内容部分、轮廓等部分。

    注:每个阶段还可能会有一些子阶段。内嵌元素的背景、边框、前景等都是在第三阶段中被绘制的。

  • 对于 RenderLayer 树包含的每个 RenderObject :各个子类不一样。

WebKit 第一次绘制网页的时候,绘制的区域等同于可视区域大小,之后 WebKit 只计算需要更新的区域,然后绘制同这些区域有交集的 RenderObject 节点(即如果更新区域跟某个 RenderLayer 节点有交集,WebKit 会继续查找 RenderLayer 树中包含的 RenderObject 子树中的特定一个或一些节点,而不是绘制整个 RenderLayer 对应的 RenderObject 子树。

WebKit 的软件渲染过程是在 Renderer 进程中进行的,而网页的显示是在 Browser 进程中的。

触发重新绘制网页:

  • 前端请求:请求从 Browser 进程发起,可能是浏览器自身的一些需求,也可能是窗口系统的请求。
  • 后端请求:由于页面自身的逻辑而发起更新部分区域的请求,如 HTML 元素或者样式的改变、动画等。

第八章 硬件加速机制#

硬件加速技术:使用 GPU 的硬件能力来帮助渲染网页。有更新请求,如果没有分层,可能需要重新绘制所有的区域;网页分层之后,部分区域的更新可能只在网页的一层或者几层,而不需要将整个网页都重新绘制。

为了节省 GPU 内存资源,硬件加速机制在 RenderLayer 树建立之后:

  • WebKit 将某些 RenderLayer 对象组合在一起,形成一个由后端存储(可能是 GPU 内存)的新层(用于之后的合成)即合成层,即需要注意 RenderLayer 对象很可能不是与最终显示出来的图形层次一一对应。
  • 将每个合成层包含的这些 RenderLayer 内容绘制在合成层的后端存储中(软件绘制或硬件绘制)
  • 由合成器将多个合成层合成起来,形成网页的最终可视化结果(一张图片),合成器能够将多个合成层按照这些层的前后顺序、合成层的 3D 变形等设置而合成一个图像结果。

哪些 RenderLayer 对象可以是合成层:

  • RenderLayer 具有 CSS 3D 属性或者 CSS 透视效果
  • RenderLayer 包含的 RenderObject 节点表示的是使用硬件加速的视频解码技术的 HTML5 video 元素
  • RenderLayer 包含的 RenderObject 节点表示的是使用硬件加速的 Canvas 2D 元素或者 WebGL 技术
  • RenderLayer 使用了 CSS 透明效果的动画或者 CSS 变换的动画
  • RenderLayer 使用了硬件加速的 CSS Filters 技术
  • RenderLayer 使用了剪裁(Clip)或者反射(Reflection)属性,并且其后代中包括了一个合成层
  • RenderLayer 有一个 Z 坐标比自己小的兄弟节点,且该节点是一个合成层

原因:合并一些 RenderLayer 层可以减少内存使用量,尽量减少合并带来的重绘性能和处理上的困难,并且对于某些 RenderLayer 对象使用单独层能够显著提升性能,如使用 WebGL 技术的 canvas 元素。

2D 图形的硬件加速机制:使用 GPU 来绘制 2D 图形的方法即为 2D 图形的硬件加速机制。2D 绘图本身使用 2D 的图形上下文,一般使用软件方式(光栅化)来绘制。2D 图形硬件加速应用场景:

  • 网页基本元素的绘制
  • canvas 元素,用来绘制 2D 图形

第九章 JavaScript 引擎#

JavaScript 语言#

解释型语言、动态语言(无类型语言)。c++ 或 Java 因为其静态语言特性在编译的时候就能够知道每个变量的类型,JavaScript 的动态特性导致运行效率比 c++ 或 Java 低得多:静态语言在编译的时候就能确定其变量的地址和类型,所以 c++ 和 Java 在实现知道所存储的成员变量(类)类型的情况下,语言解释系统只要利用数组和位移来存储这些变量和方法的地址,位移信息使它只需要几个机器语言指令就可以存取变量、找出变量或执行其他任务。而 JavaScript 中变量及其属性都是在执行阶段动态创建的,以对象为例其保存的方式都是属性名-属性值对的方式,属性名采用字符串保存,存在内容冗余,随着对象的增多带来巨大的空间浪费。

从获取对象属性值的具体位置(相对于对象基地址的偏移位置)角度,JavaScript 与 c++ 的区别在于:

  • 编译确定位置:c++ 编译这些位置的偏移信息都是编译器在编译时决定,编译成本地代码(汇编代码)之后对象的属性和偏移信息都计算完成。JavaScript 没有类型故只能在执行阶段确定,并且在执行时可以修改对象的属性(添加或删除属性本身)。
  • 偏移信息共享:编译时已经确定了各个对象的偏移量,访问时只需要按照这个偏移量,故不能再执行的时候动态改变类型。JavaScript 中每个对象都是自描述,属性和位置偏移信息都包含在自身结构中。
  • 偏移信息查找:c++ 中在编译时对使用到的某类型的成员变量直接设置偏移量,查找偏移地址简单。JavaScript 中使用一个对象需要通过属性名匹配查找到对应的值,比较耗时。

对象属性访问普遍并且频繁,通过偏移量访问并且知道类型只需要使用少数几个汇编指令就能完成,相较而言属性名匹配需要耗费较长的时间并且浪费了额外的内存空间。

JIT (Just-In-Time,推动 JavaScript 运行速度提高的一大利器):

  • 作用是解决解释性语言的性能问题
  • 主要思想:当解释器将源代码解释成内部表示的时候,将其中的一些字节码(使用率高的部分)转成本地代码(汇编代码),可以直接被 CPU 执行而不需解释执行。(使用于 Java 虚拟机、JavaScriptCore、V8 等)

JavaScript 引擎和渲染引擎#

能够将 JavaScript 代码处理并执行的运行环境。

编译原理基础:

  • c/c++:使用编译器直接将其编译成本地代码,开发人员在代码编写之后实施,用户使用编译好的本地代码(汇编代码),被系统的加载器加载执行,由操作系统调度 CPU 直接执行。

    c++编译
  • python:处理脚本语言通常做法是开发者将写好的代码直接交给用户,用户使用脚本的解释器将脚本文件加载然后解释执行。通常情况下脚本语言不需要开发人员去编译脚本代码,主要是因为脚本语言对使用场景和性能的要求与其他类型语言不同。

    解释器执行
  • java:通过编译器生成的是字节码,字节码是跨平台的一种中间表示,不等同于本地代码,可以在不同操作系统上运行。通过 java 的运行环境( java 虚拟机)加载字节码,使用解释器执行这些字节码。通过 JIT 技术提高执行效率。

    java 编译
  • JavaScript:现在的做法是由解释器将源代码转变成抽象语法树,然后将抽象语法树转成中间表示(字节码),通过 JIT 技术转换提高执行效率(也可以直接从抽象语法树生成本地代码)。

    javascript 解释

java 与 JavaScript 编译区别:

  • 类型:JavaScript 无类型,对于对象的表示和属性的访问比 java 存在更大的性能损失。
  • Java 通常将在编译阶段将源代码编译成字节码,这与执行阶段分开,故从源代码到字节码这段时间长短不是特别重要,可以尽可能生成高效字节码;JavaScript 从源代码到字节码到本地代码都是在网页和 JavaScript 文件下载后同执行阶段一切在网页的加载和渲染过程实施,对处理时间有较高的要求。

JavaScript 引擎与渲染引擎:JavaScript 引擎提供调用接口给渲染引擎以便渲染引擎使用 JavaScript 引擎来处理 JavaScript 代码并获取结果,JavaScript 引擎还需要提供桥接的接口,渲染引擎可以根据桥接的接口来提供让 JavaScript 访问 DOM 的能力。弊端:两种引擎通过桥接接口访问 DOM 结构给性能带来一个重大的损失,因为每次 JavaScript 代码访问 DOM 都需要通过复杂低效的桥接接口来完成,而访问 DOM 树又具有普遍性。

V8 引擎#

工作原理

  • 数据表示:V8 中数据的表示分成两个部分

    • 第一部分是数据的实际内容,变长,内容类型多样(如 String、对象等)
    • 第二部分是数据的句柄,大小固定,句柄中包含指向数据的指针

    V8 进行垃圾回收需要移动这些数据内容,如果直接使用使用指针就会出现问题或者需要较大开销,使用句柄只需要将句柄中的指针修改即可,其本身没有发生变化。除极少数的数据(如整形数据)存在句柄中(快速访问),其他的内容都是从堆中申请内存来存储(受限于句柄的大小和变长原因)。一个句柄类 Handle 对象大小为 4 字节( 32 位机器)或 8 字节( 64 位机器),整数(小整数,只有 31 位表示,标志位为 0)直接存储,其他类型( 30 位表示其指针,标志位 01,因为堆中存放的对象都是 4 字节对齐,指向它们的指针的最后两位都是 00,其实最后两位不需要,这里用来表示句柄中包含数据的类型)的指针指向堆中的数据。

  • 工作过程

    编译阶段:首先将源代码转变成抽象语法树,之后通过 JIT 编译器的全代码生成器从抽象语法树直接生成本地代码,没有转变成字节码或者其他中间表示是因为减少转换时间,缺点是没有中间比碍事会减少优化的机会,同时某些 JavaScript 直接使用解释器更为合适(没必要生成本地代码)。

     V8 引擎处理源代码到本地代码的过程

    延迟思想:很多 JavaScript 代码的编译直到运行的时候被调用到才会发生,减少时间开销。

  • 优化回滚(Deoptimization)

    V8 为了性能优化而引入的高效编译器通常会做大胆预测,如代码稳定、变量类型不会改变等,但现实不是这样,优化回滚就是用来将这些错误回滚到之前的一般情况。

  • 隐藏类和内嵌缓存

    隐藏类(将本来需要通过字符串匹配查找属性值得算法改进为类似 c++ 编译器的偏移位置机制来实现):将对象划分为不同的组,对于相同的组(组内对象拥有相同的属性名),将这些属性名和对应的偏移位置保存在一个隐藏类中,组内所有对象共享该信息。访问这些对象属性的时候可以根据隐藏类的偏移值得知其位置。

    内嵌缓存(可以避免方法和属性被存储的时候出现的因哈希表查找而带来的问题):将之前查找的隐藏类和偏移值保存下来,下次查找的时候首先比较当前对象是否也是之前的隐藏类,是的话可以直接使用之前缓存的偏移值,减少查找表的时间(即获取隐藏类的地址、根据属性名查找偏移值、计算该属性地址的时间)。

  • 内存管理

    • V8 内存的划分

      内存划分由 Zone 类完成,它的特点是管理一系列小内存,如果用户想使用一系列小内存并且这些小内存生命周期类似,就可以使用一个 Zone 对象,小内存都是从 Zone 对象中申请,Zone 对象首先自己申请一块内存然后管理和分配一些小内存,一块小内存分配之后不能被单独回收,而只能由 Zone 一次性回收其分配的所有小块内存。

    • V8 对于 JavaScript 代码的垃圾回收机制

      V8 使用堆来管理 JavaScript 使用的数据、其生成的代码、哈希表等。V8 将堆分成了三个部分:

      • 年轻分带

        主要为新创建的对象分配内存空间,年轻分代有两个部分,一个用来分配,另一个用来在回收的时候负责将之前还需要保留的对象复制保存。

      • 年老分带

        根据需要将年老的对象、指针、代码等数据使用较少地做垃圾回收

      • 大对象

        用来为需要使用较多内存的大对象分配内存,每个页面只分配一个对象

      参考 nodeJS 详细介绍

  • 快照

    V8 引擎开始启动需要加载很多内置的全局对象并建立内置的函数,快照机制就是将这些内置的对象和函数加载之后的内存保存并序列化,同时也可以将需要的 js 文件序列化,以减少以后的处理时间。

JavaScriptCore 引擎#

JavaScriptCore 是 WebKit 中默认的 JavaScript 引擎

架构和模块

  • 数据表示:同样使用句柄来表示数据,不同于 V8 之处在于 JavaScriptCore 都是用 64 位来表示句柄。32 位平台上,对于整数、布尔和指针使用前 32 位标记它们,后 32 位用来表示这些数据,对于双浮点数,前 32 位在区间 FFFFFFF8~00000000 都用来表示浮点数(还有一些用来标记整数、布尔、指针),比原来的双浮点表示范围小一些。64 位平台上,对于指针使用前 16 位标示,后 48 为表示地址,双浮点数前 16 位在 0001~FFFE 都用来表示浮点数,整数使用前 32 位来标示。

    句柄的定义和各种类型的表示方式
  • 模块

    • 使用字节码的中间表示,有了字节码之后不再需要 JavaScript 源代码( V8 中进行优化时需要从 JavaScript 源代码重新开始)。
    • 字节码解释器,能够解释字节码生成结果。一些 JavaScript 代码不需要经过很强的优化,直接执行即可,复杂处理带来的额外开销可能抵消优化带来的好处。
    • JIT:通过在字节码执行期间收集的热点函数,将对应函数的字节码翻译成本地代码,同时这一阶段也有信息收集以进一步优化本地代码。

    通常优化性能越好需要的分析和生成代码的时间就越长,等待的时间就越长,故不直接使用优化性能最好的编译器。

作者:EGBDFACE

出处:https://www.cnblogs.com/EGBDFACE/p/17120574.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   EGBDFACE  阅读(265)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示