chrome渲染引擎blink如何工作

眨眼的工作原理

bit.ly/how-blink-works

作者:haraken @

最后更新:2018年8月14日

状态:PUBLIC

 

在眨眼上工作并不容易。对于新的Blink开发人员而言,这并不容易,因为已经引入了许多特定于Blink的概念和编码约定来实现非常快速的渲染引擎。即使对于有经验的Blink开发人员来说,这也不容易,因为Blink庞大且对性能,内存和安全性极为敏感。

 

本文档旨在提供1 万英尺的“ Blink的工作原理”概述,我希望它将有助于Blink开发人员快速熟悉该体系结构:

 

  • 该文档不是有关Blink详细架构和编码规则(可能会更改和过时)的详尽教程。相反,该文档简明扼要地描述了Blink的基本原理(短期内不太可能改变),并指出了您想了解更多信息时可以阅读的资源。
  • 该文档未解释特定功能(例如ServiceWorkers,编辑)。而是该文档解释了广泛的代码库所使用的基本功能(例如,内存管理,V8 API)。

 

有关Blink开发的更多常规信息,请参阅Chromium Wiki页面

 

眨眼做什么

流程/线程架构

工艺流程

线程数

闪烁的初始化

目录结构

内容公共API和Blink公共API

目录结构和依赖性

WTF

内存管理

任务调度

页面,框架,文档,DOMWindow等

概念

OOPIF

分离的框架/文件

Web IDL绑定

V8和闪烁

隔离,上下文,世界

V8 API

V8包装器

渲染管线

有什么问题吗

 

眨眼做什么

Blink是Web平台的渲染引擎。粗略地说,Blink实现了所有在浏览器选项卡中呈现内容的内容:

 

  • 实施Web平台的规范(例如HTML标准),包括DOM,CSS和Web IDL
  • 嵌入V8并运行JavaScript
  • 从基础网络堆栈请求资源
  • 建立DOM树
  • 计算样式和布局
  • 嵌入Chrome合成器并绘制图形

 

许多用户(例如Chromium,Android WebView和Opera)通过内容公开API嵌入了Blink 

 

从代码库的角度来看,“闪烁”通常是指// third_party / blink /。从项目角度来看,“闪烁”通常表示实现Web平台功能的项目。实现Web平台功能的代码跨越// third_party / blink /,// content / renderer /,// content /浏览器/和其他地方。

流程/线程架构

工艺流程

铬具有多工艺体系结构。Chromium具有一个浏览器进程和N个沙盒渲染器进程。闪烁在渲染器进程中运行。

 

创建了多少个渲染器进程?出于安全原因,隔离跨站点文档之间的内存地址区域非常重要(这称为Site Isolation)。从概念上讲,每个渲染器过程最多应专用于一个站点。但是实际上,当用户打开太多标签页或设备没有足够的RAM时,有时将每个渲染器进程限制在一个站点上有时会很繁琐。然后,渲染器进程可以由从不同站点加载的多个iframe或标签共享。这意味着一个选项卡中的iframe可以由不同的渲染器进程托管,而不同选项卡中的iframe可以由相同的渲染器进程托管。渲染器进程,iframe和Tab之间没有1:1映射

 

假定渲染器进程在沙箱中运行,则Blink需要要求浏览器进程调度系统调用(例如,文件访问,播放音频)并访问用户配置文件数据(例如Cookie,密码)。这种浏览器-渲染器过程通信是由Mojo实现的。(注意:过去我们使用的是Chromium IPC,但仍然有很多地方在使用它。但是,它已被弃用,并在后台使用Mojo。)在Chromium方面,服务化正在进行中,并将浏览器过程抽象为一组“服务。从Blink角度来看,Blink可以仅使用Mojo与服务和浏览器进程进行交互。

 

如果您想了解更多信息:

 

  • 多进程架构
  • Blink中的Mojo编程:platform / mojo / MojoProgrammingInBlink.md

线程数

在渲染器进程中创建了多少个线程?

 

眨眼有一个主线程,N个工作线程和几个内部线程。

 

几乎所有重要的事情都在主线程上发生。所有JavaScript(工人除外),DOM,CSS,样式和布局计算都在主线程上运行。假设主要是单线程体系结构,Blink进行了高度优化以最大化主线程的性能。

 

眨眼可能会创建多个工作线程来运行Web WorkersServiceWorkerWorklets

 

Blink和V8可能会创建几个内部线程来处理webaudio,数据库,GC等。

 

对于跨线程通信,必须使用通过PostTask API传递消息。不建议使用共享内存编程,除了出于性能原因确实需要使用它的几个地方。这就是为什么您在Blink代码库中看不到很多MutexLocks的原因。

 

如果您想了解更多信息:

 

闪烁的初始化和完成

眨眼由BlinkInitializer :: Initialize()初始化。在执行任何Blink代码之前必须调用此方法。

 

另一方面,Blink从未完成。也就是说,渲染器进程被强制退出而不进行清理。原因之一是性能。另一个原因是,通常很难以一种有序的方式清理渲染器进程中的所有内容(这是不值得的工作)。

目录结构

内容公共API和Blink公共API

内容公共API是使嵌入程序嵌入呈现引擎的API层。内容公共API必须小心维护,因为它们会暴露在嵌入程序中。

 

眨眼公共API是将// // third_party / blink /的功能公开给Chromium的API层。该API层只是从WebKit继承的历史工件。在WebKit时代,Chromium和Safari共享WebKit的实现,因此需要API层才能将WebKit的功能公开给Chromium和Safari。既然Chromium是// third_party / blink /的唯一嵌入者,那么API层就没有意义了。通过将网络平台代码从Chromium移到Blink(该项目称为Onion Soup),我们正在积极减少Blink公共API的数量。

 

目录结构和依赖性

// third_party / blink /具有以下目录。有关这些目录的更详细定义,请参阅此文档

 

  • 平台/
    • Blink的较低级功能的集合,这些功能是从整体内核中剔除的。例如,几何和图形工具。
  • 核心/和模块/
    • 规范中定义的所有Web平台功能的实现。core /实现与DOM紧密结合的功能。模块/实现更多独立功能。例如webaudio,indexeddb。
  • 绑定/核心/和绑定/模块/
    • 从概念上讲,bindings / core /是core /的一部分,而bindings / modules /是modules /的一部分。大量使用V8 API的文件被放在bindings / {core,modules}中。
  • 控制器/
    • 一组使用core /和modules /的高级库。例如devtools前端。

 

依存关系按以下顺序流动:

 

  • 铬=>控制器/ =>模块/和绑定/模块/ =>核心/和绑定/核心/ =>平台/ =>底层基元,例如// base,// v8和// cc

 

Blink仔细维护暴露于// third_party / blink /的低级基元列表。

 

如果您想了解更多信息:

 

WTF

WTF是一个“特定于眨眼的基础”库,位于platform / wtf /。我们正在尝试尽可能多地统一Chromium和Blink之间的编码原语,因此WTF应该很小。需要此库是因为确实需要针对Blink的工作量和Oilpan(Blink GC)优化许多类型,容器和宏。如果类型是在WTF中定义的,则Blink必须使用WTF类型而不是// base或std库中定义的类型。最受欢迎的是矢量,哈希集,哈希图和字符串。眨眼应该使用WTF :: Vector,WTF :: HashSet,WTF :: HashMap,WTF :: String和WTF :: AtomicString而不是std :: vector,std :: * set,std :: * map和std :: string 。

 

如果您想了解更多信息:

 

内存管理

就Blink而言,您需要关心三个内存分配器:

 

 

您可以使用USING_FAST_MALLOC()在PartitionAlloc的堆上分配一个对象:

 

类SomeObject {

  USING_FAST_MALLOC(SomeObject);

  静态std :: unique_ptr <SomeObject> Create(){

    返回std :: make_unique <SomeObject>(); //分配在PartitionAlloc的堆上。

}

};

 

由PartitionAlloc分配的对象的生存期应由scoped_refptr <>或std :: unique_ptr <>管理。强烈建议不要手动管理生命周期。闪烁禁止手动删除

 

您可以使用GarbageCollected在Oilpan的堆上分配一个对象:

 

类SomeObject:公共GarbageCollected <SomeObject> {

  静态SomeObject * Create(){

    返回新的SomeObject; //分配在Oilpan的堆上。

  }

};

 

Oilpan分配的对象的生存期由垃圾收集自动管理。您必须使用特殊的指针(例如Member <>,Persistent <>)将对象保存在Oilpan的堆上。请参阅此API参考以熟悉有关Oilpan的编程限制。最重要的限制是不允许您在油锅对象的析构函数中触摸任何其他油锅对象(因为无法保证销毁顺序)。

 

如果您既不使用USING_FAST_MALLOC()也不使用GarbageCollected,则在系统malloc的堆上分配对象。在眨眼中强烈建议不要这样做。所有Blink对象应由PartitionAlloc或Oilpan分配,如下所示:

 

  • 默认情况下使用Oilpan。
  • 仅在以下情况下才使用PartitionAlloc:1)对象的生存期非常明确并且std :: unique_ptr <>或scoped_refptr <>足够,2)在Oilpan上分配对象会带来很多复杂性,或者3)在Oilpan上分配对象会导致给垃圾收集运行时带来了不必要的压力。

 

无论使用PartitionAlloc还是Oilpan,都必须非常小心,不要创建悬空的指针(注意:强烈建议不要使用原始指针)或内存泄漏。

 

如果您想了解更多信息:

 

任务调度

为了提高渲染引擎的响应速度,Blink中的任务应尽可能异步执行。不鼓励同步IPC / Mojo和任何其他可能花费几毫秒的操作(尽管某些操作是不可避免的,例如用户的JavaScript执行)。

 

呈现器进程中的所有任务都应使用正确的任务类型发布到Blink Scheduler,如下所示:

 

//使用kNetworking的任务类型将任务发布到框架的调度程序

frame-> GetTaskRunner(TaskType :: kNetworking)-> PostTask(…,WTF :: Bind(&Function));

 

Blink Scheduler维护多个任务队列,并巧妙地对任务进行优先级排序,以最大化用户感知的性能。重要的是要指定适当的任务类型,以使Blink Scheduler能够正确,智能地调度任务。

 

如果您想了解更多信息:

 

  • 如何发布任务:platform / scheduler / PostTask.md

页面,框架,文档,DOMWindow等

概念

页面,框架,文档,ExecutionContext和DOMWindow是以下概念:

 

  • 页面与选项卡的概念相对应(如果未启用下面说明的OOPIF)。每个渲染器进程可能包含多个选项卡。
  • 框架对应于框架(主框架或iframe)的概念。每个页面可以包含一个或多个以树状层次结构排列的框架。
  • DOMWindow对应于JavaScript中的窗口对象。每个框架都有一个DOMWindow。
  • Document对应于JavaScript中的window.document 对象。每个框架都有一个文档。
  • ExecutionContext是一个抽象文档(用于主线程)和WorkerGlobalScope(用于工作线程)的概念。

 

渲染过程:页面= 1:N

 

页:框架= 1:M.

 

框架:DOMWindow:文档(或ExecutionContext)= 1:1:1在任何时间点,但映射可能随时间而变化。例如,考虑以下代码:

 

iframe.contentWindow.location.href =“ https://example.com”;

 

在这种情况下,将为https://example.com创建一个新的DOMWindow和一个新的Document 。但是,可以重复使用该框架。

 

(注意:确切地说,在某些情况下会创建一个新的Document,但是DOMWindow和Frame会被重用。甚至还有一些更复杂的情况。)

 

如果您想了解更多信息:

 

  • 核心/框架/FrameLifecycle.md

进程外iframe(OOPIF)

站点隔离使事情变得更加安全,但更加复杂。🙂站点隔离的想法是为每个站点创建一个渲染器进程。(网站是页面的可注册域+ 1标签及其URL方案。例如,https://mail.example.comhttps://chat.example.com在同一网站中,但https:// noodles.comhttps://pumpkins.com都没有。)如果一个页面包含一个跨站点IFRAME,该页面可以由两个渲染过程托管。考虑以下页面:

 

<!– https://example.com –>

<身体>

<iframe src =” https://example2.com”> </ iframe>

</ body>

 

主框架和<iframe>可以由不同的渲染器进程托管。渲染器进程本地的帧由LocalFrame表示,而不是渲染器进程本地的帧由RemoteFrame表示。

 

从主框架的角度来看,主框架是LocalFrame,而<iframe>是RemoteFrame。从<iframe>的角度来看,主框架是RemoteFrame,而<iframe>是LocalFrame。

 

本地框架和远程框架(可能存在于不同的渲染器进程中)之间的通信是通过浏览器进程进行处理的。

 

如果您想了解更多信息:

 

分离的框架/文件

相框/文档可能处于分离状态。考虑以下情况:

 

doc = iframe.contentDocument;

iframe.remove(); //将iframe与DOM树分离。

doc.createElement(“ div”); //但是您仍然可以在分离的框架上运行脚本。

 

棘手的事实是,您仍然可以在分离的框架上运行脚本或DOM操作。由于框架已经分离,大多数DOM操作将失败并引发错误。不幸的是,分离框架上的行为在浏览器之间并不能真正实现互操作,在规范中也没有明确定义。基本上,人们期望JavaScript可以继续运行,但是大多数DOM操作应该会因某些适当的异常而失败,例如:

 

无效someDOMOperation(…){

  if(!script_state _-> ContextIsValid()){//框架已经分离

    …;//设置例外等

    返回;

}

}

 

这意味着在通常情况下,当框架分离时,Blink需要执行一系列清理操作。您可以通过从ContextLifecycleObserver继承来做到这一点,如下所示:

 

类SomeObject:公共GarbageCollected <SomeObject>,公共ContextLifecycleObserver {

  void ContextDestroyed()覆盖{

    //在此进行清理操作。

}

  〜SomeObject(){

    //在这里进行清理操作不是一个好主意,因为现在进行清理已经太迟了。此外,不允许析构函数接触Oilpan堆上的任何其他对象。

  }

};

Web IDL绑定

当JavaScript访问node.firstChild时将调用node.h 中的Node :: firstChild()。它是如何工作的?让我们看一下node.firstChild的工作方式。

 

首先,您需要根据规范定义一个IDL文件:

 

// node.idl

接口Node:EventTarget {

  […]只读属性Node?第一个孩子;

};

 

Web IDL的语法在Web IDL规范中定义[…] 称为IDL扩展属性。一些IDL扩展属性是在Web IDL规范中定义的,而另一些是特定于Blink的IDL扩展属性。除了特定于闪烁的IDL扩展属性外,IDL文件应以符合规范的方式编写(即,仅从规范中复制并粘贴)。

 

其次,您需要为Node定义一个C ++类,并为firstChild实现一个C ++ getter:

 

class EventTarget:public Sc​​riptWrappable {//所有暴露给JavaScript的类都必须从ScriptWrappable继承。

…;

};

 

类Node:public EventTarget {

  DEFINE_WRAPPERTYPEINFO(); //所有具有IDL文件的类都必须具有此宏。

  节点* firstChild()const {return first_child_; }

};

 

在通常情况下,就是这样。生成node.idl时,IDL编译器会自动为Node接口和Node.firstChild生成Blink-V8绑定。自动生成的绑定是在//src/out/{Debug,Release}/gen/third_party/blink/renderer/bindings/core/v8/v8_node.h中生成的。当JavaScript调用node.firstChild时,V8会调用v8_node.h中的V8Node :: firstChildAttributeGetterCallback(),然后会调用您在上面定义的Node :: firstChild()。

 

如果您想了解更多信息:

 

V8和闪烁

隔离,上下文,世界

当您编写涉及V8 API的代码时,了解隔离,上下文和世界的概念很重要。它们分别在代码库中由v8 :: Isolate,v8 :: Context和DOMWrapperWorld表示。

 

隔离对应于物理线程。隔离:闪烁中的物理线程= 1:1。主线程具有自己的隔离。辅助线程具有其自己的隔离。

 

上下文对应于全局对象(在使用框架的情况下,它是框架的窗口对象)。由于每个框架都有其自己的窗口对象,因此渲染器进程中存在多个上下文。调用V8 API时,必须确保您使用的是正确的上下文。否则,v8 :: Isolate :: GetCurrentContext()将返回错误的上下文,在最坏的情况下,它将最终导致对象泄漏并导致安全问题。

 

World是支持Chrome扩展程序的内容脚本的概念。世界与Web标准中的任何内容都不对应。内容脚本希望与网页共享DOM,但是出于安全原因,必须将内容脚本的JavaScript对象与网页的JavaScript堆隔离。(还必须将一个内容脚本的JavaScript堆与另一个内容脚本的JavaScript堆隔离。)为了实现隔离,主线程为网页创建了一个主世界,为每个内容脚本创建了一个隔离世界。主世界和孤立世界可以访问相同的C ++ DOM对象,但是它们的JavaScript对象是孤立的。通过为一个C ++ DOM对象创建多个V8包装器来实现这种隔离。也就是说,每个世界一个V8包装器。

 

上下文,世界和框架之间有什么关系?

 

想象一下,在主线程上有N个世界(一个主世界+(N – 1)个孤立世界)。然后,一个框架应具有N个窗口对象,每个窗口对象用于一个世界。上下文是与窗口对象相对应的概念。这意味着,当我们有M个框架和N个世界时,我们有M * N个上下文(但是这些上下文是惰性创建的)。

 

如果是工人,则只有一个世界和一个全局对象。因此,只有一个上下文。

 

同样,当您使用V8 API时,应该非常小心使用正确的上下文。否则,您最终将在孤立的世界之间泄漏JavaScript对象并造成安全灾难(例如,来自A.com的扩展程序可以操纵来自B.com的扩展程序)。

 

如果您想了解更多信息:

 

V8 API

//v8/include/v8.h中定义了许多V8 API 。由于V8 API是低级的并且难以正确使用,因此platform / bindings /提供了一堆包装V8 API的帮助程序类。您应该考虑尽可能使用助手类。如果您的代码必须大量使用V8 API,则应将文件放在bindings / {core,modules}中。

 

V8使用句柄指向V8对象。最常见的句柄是v8 :: Local <>,用于从计算机堆栈指向V8对象。在计算机堆栈上分配v8 :: HandleScope之后,必须使用v8 :: Local <>。v8 :: Local <>不应在机器堆栈之外使用:

 

void function(){

  v8 :: HandleScope范围;

  v8 :: Local <v8 :: Object> object =…; // 这是对的。

}

 

类SomeObject:公共GarbageCollected <SomeObject> {

  v8 :: Local <v8 :: Object> object_; //这是错误的。

};

 

如果要从计算机堆栈外部指向V8对象,则需要使用包装器跟踪。但是,您必须非常小心,不要用它创建参考循环。通常,V8 API很难使用。如果您不确定自己在做什么,请询问blink-review-bindings @

 

如果您想了解更多信息:

 

  • 如何使用V8 API和帮助程序类:platform / bindings / HowToUseV8FromBlink.md

V8包装器

每个C ++ DOM对象(例如Node)都有其对应的V8包装器。确切地说,每个C ++ DOM对象每个世界都有其对应的V8包装器。

 

V8包装器对其相应的C ++ DOM对象有很强的引用。但是,C ++ DOM对象仅对V8包装程序具有弱引用。因此,如果您想让V8包装器存活一段时间,则必须明确地做到这一点。否则,将过早收集V8包装器,并且V8包装器上的JS属性将丢失。

 

div = document.getElementbyId(“ div”);

child = div.firstChild;

child.foo =“酒吧”;

child = null;

GC(); //如果不执行任何操作,则| firstChild |的V8包装器 由GC收集。

assert(div.firstChild.foo ===“ bar”); // …这将失败。

 

如果我们什么都不做,GC会收集child ,因此child.foo 会丢失。为了使div.firstChild 的V8包装器保持活动状态,我们必须添加一种机制,“ 只要div 所属的DOM树可以从V8到达,则使div.firstChild 的V8包装器保持活动状态”。

 

有两种方法可以使V8包装器保持活动状态:ActiveScriptWrappable包装器跟踪

 

如果您想了解更多信息:

 

渲染管线

从将HTML文件发送到Blink到在屏幕上显示像素还有很长的一段路要走。渲染管道的结构如下。

 

阅读这个出色的资料,以了解渲染管线的每个阶段的功能。(我认为我能写出比甲板更好的解释🙂

 

如果您想了解更多信息,请联系:GC收集。

 

assert(div.firstChild.foo ===“ bar”); // …这将失败。

 

如果我们什么都不做,GC会收集child,因此child.foo会丢失。为了使div.firstChild的V8包装器保持活动状态,我们必须添加一种机制,“只要div所属的DOM树可以从V8到达,则使div.firstChild的V8包装器保持活动状态”。

 

有两种方法可以使V8包装器保持活动状态:ActiveScriptWrappable和包装器跟踪。

 

如果您想了解更多信息:

 

如何管理V8包装器的生命周期:bindings / core / v8 / V8Wrapper.md

 

如何使用包装程序跟踪:platform / bindings / TraceWrapperReference.md

 

渲染管线

从将HTML文件发送到Blink到在屏幕上显示像素还有很长的一段路要走。渲染管道的结构如下。