DevSupport

Chromium内核原理之blink内核工作解密

2019-02-13  本文已影响30人  码上就说

《Chromium内核原理之blink内核工作解密》
《Chromium内核原理之多进程架构》
《Chromium内核原理之进程间通信(IPC)》
《Chromium内核原理之网络栈》
《Chromium内核原理之网络栈HTTP Cache》
《Chromium内核原理之Preconnect》
《Chromium内核原理之Prerender》
《Chromium内核原理之cronet独立化》

概要
1.Blink做了什么
2.进程/线程架构

2.1 进程
2.2 线程
2.3 Blink初始化

3.目录结构

3.1 内容公共API和Blink公共API
3.2 目录结构和依赖项
3.3 WTF

4.内存管理
5.任务调度
6.Page, Frame, Document, DOMWindow等

6.1 概念
6.2 Out-of-Process iframes (OOPIF)
6.3 分离的 Frame/ Document

7.Web IDL绑定
8.V8和Blink

8.1 Isolate, Context, World
8.2 V8 API
8.3 V8 wrappers

9.渲染管道

概要:

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

1.Blink做了什么

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

Blink通过内容公共API嵌入许多客户,如Chromium,Android WebView和Opera。


blink_content_arch.jpg

从代码库的角度来看,“Blink”通常表示// third_party / blink /。从项目角度来看,“Blink”通常表示实现Web平台功能的项目。实现Web平台功能的代码跨度为// third_party / blink /,// content / renderer /,// content / browser /和其他位置。

2.进程/线程架构

2.1 进程

Chromium具有多进程架构。 Chromium有一个浏览器进程和N个沙盒渲染器进程。 Blink在渲染器进程中运行。

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

假定渲染器进程在沙箱中运行,则Blink需要请求浏览器进程分派系统调用(例如,文件访问,播放音频)和访问用户简档数据(例如,cookie,密码)。这个浏览器渲染器进程通信由Mojo实现。 (注意:过去我们使用的是Chromium IPC,但仍有一些地方正在使用它。但是,它已被弃用,并在引擎盖下使用Mojo。)在Chromium方面,Servicification正在进行并将浏览器进程抽象为一组“服务。从Blink的角度来看,Blink可以使用Mojo与服务和浏览器进程进行交互。


blink_process.jpg
2.2 线程

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

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

几乎所有重要的事情都发生在主线程上。所有JavaScript(工作者除外),DOM,CSS,样式和布局计算都在主线程上运行。假设大多数是单线程架构,Blink经过高度优化以最大化主线程的性能。

Blink可能会创建多个工作线程来运行Web Workers,ServiceWorker和Worklet。

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

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


thread_arc.jpg
2.3 Blink初始化

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

另一方面,Blink永远不会完成;即渲染器过程被强制退出而不被清理。一个原因是表现。另一个原因是,通常很难以优雅的顺序清理渲染器过程中的所有内容(并且不值得付出努力)。

3.目录结构

3.1 内容公共API和Blink公共API

内容公共API是API层,它使嵌入器能够嵌入呈现引擎。必须仔细维护内容公共API,因为它们暴露给嵌入器。

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


public_api.jpg
3.2 目录结构和依赖项

/ third_party / blink /具有以下目录。有关这些目录的更详细定义,请参阅此文档:https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/README.md

依赖关系按以下顺序:
Chromium => controller/ => modules/ and bindings/modules/ => core/ and bindings/core/ => platform/ => low-level primitives such as //base, //v8 and //cc

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

3.3 WTF

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

4.内存管理

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

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

class SomeObject {
  USING_FAST_MALLOC(SomeObject);
  static std::unique_ptr<SomeObject> Create() {
    return std::make_unique<SomeObject>();  // Allocated on PartitionAlloc's heap.
  }
};

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

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

class SomeObject : public GarbageCollected<SomeObject> {
  static SomeObject* Create() {
    return new SomeObject;  // Allocated on Oilpan's heap.
  }
};

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

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

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

5.任务调度

为了提高渲染引擎的响应能力,Blink中的任务应尽可能异步执行。不鼓励同步IPC / Mojo和可能需要几毫秒的任何其他操作。

渲染器进程中的所有任务都应该使用适当的任务类型发布到Blink Scheduler,如下所示:

// Post a task to frame's scheduler with a task type of kNetworking
frame->GetTaskRunner(TaskType::kNetworking)->PostTask(..., WTF::Bind(&Function));

Blink Scheduler维护多个任务队列,并巧妙地确定任务的优先级,以最大限度地提高用户感知的性能。指定正确的任务类型以使Blink Scheduler正确而巧妙地安排任务非常重要。

6.Page, Frame, Document, DOMWindow等

6.1 概念

Page,Frame,Document,ExecutionContext和DOMWindow是以下概念:

渲染器过程:Page = 1:N。
Page : Frame = 1 : M.

Frame : DOMWindow : Document (or ExecutionContext) = 1:1:1在任何时间点,但映射可能会随时间而变化。例如,请考虑以下代码:

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

在这种情况下,将为https://example.com创建新的DOMWindow和新文档。但是,帧可以重复使用。

6.2 Out-of-Process iframes (OOPIF)

站点隔离使事情更安全,但更复杂。 :)站点隔离的想法是为每个站点创建一个渲染器进程。 (网站是网页的可注册域名+ 1标签及其网址方案。例如,https://mail.example.comhttps://chat.example.com位于同一网站,但https:// noodles.comhttps://pumpkins.com不是。)如果一个页面包含一个跨站点iframe,那么该页面可能由两个渲染器进程托管。请考虑以下页面:

<!-- https://example.com -->
<body>
<iframe src="https://example2.com"></iframe>
</body>

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

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

LocalFrame和RemoteFrame(可能存在于不同的渲染器进程中)之间的通信通过浏览器进程处理。

6.3 分离的 Frame/ Document

Frame/ Document可能处于分离状态。考虑以下情况:

doc = iframe.contentDocument;
iframe.remove();  // The iframe is detached from the DOM tree.
doc.createElement("div");  // But you still can run scripts on the detached frame.

棘手的事实是您仍然可以在分离的帧上运行脚本或DOM操作。由于帧已经分离,大多数DOM操作都会失败并抛出错误。不幸的是,分离帧上的行为在浏览器之间并不真正可互操作,也没有在规范中明确定义。基本上期望JavaScript应该继续运行,但是大多数DOM操作都应该失败并带有一些适当的例外,例如:

void someDOMOperation(...) {
  if (!script_state_->ContextIsValid()) { // The frame is already detached
    …;  // Set an exception etc
    return;
  }
}

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

class SomeObject : public GarbageCollected<SomeObject>, public ContextLifecycleObserver {
  void ContextDestroyed() override {
    // Do clean-up operations here.
  }
  ~SomeObject() {
    // It's not a good idea to do clean-up operations here because it's too late to do them. Also a destructor is not allowed to touch any other objects on Oilpan's heap.
  }
};

7.Web IDL绑定

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

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

// node.idl
interface Node : EventTarget {
  [...] readonly attribute Node? firstChild;
};

Web IDL的语法在Web IDL规范中定义。 [...]称为IDL扩展属性。一些IDL扩展属性在Web IDL规范中定义,而其他属性是特定于Blink的IDL扩展属性。除了特定于Blink的IDL扩展属性外,IDL文件应以特定的方式编写(即只需从规范中复制和粘贴)。

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

class EventTarget : public ScriptWrappable {  // All classes exposed to JavaScript must inherit from ScriptWrappable.
  ...;
};

class Node : public EventTarget {
  DEFINE_WRAPPERTYPEINFO();  // All classes that have IDL files must have this macro.
  Node* 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()。

8.V8和Blink

8.1 Isolate, Context, World

当您编写涉及V8 API的代码时,了解Isolate,Context和World的概念非常重要。它们分别由代码库中的v8 :: Isolate,v8 :: Context和DOMWrapperWorld表示。

Isolate对应于物理线程。 Isolate : physical thread in Blink = 1 : 1。主线程有自己的隔离。工作线程有自己的隔离。

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

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

v8_blink_arch.jpg
Context,World和Frame之间有什么关系?

想象一下,主线上有N个世界(一个主要世界+(N - 1)个孤立的世界)。然后一个Frame应该有N个窗口对象,每个窗口对象用于一个世界。上下文是对应于窗口对象的概念。这意味着当我们有M帧和N个世界时,我们有M * N上下文(但是上下文是懒洋洋地创建的)。

对于worker,只有一个世界和一个全球对象。因此,只有一个上下文。

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

8.2 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 scope;
  v8::Local<v8::Object> object = ...;  // This is correct.
}

class SomeObject : public GarbageCollected<SomeObject> {
  v8::Local<v8::Object> object_;  // This is wrong.
};
8.3 V8 wrappers

每个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 = "bar";
child = null;
gc();  // If we don't do anything, the V8 wrapper of |firstChild| is collected by the GC.
assert(div.firstChild.foo === "bar");  //...and this will fail.

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

9.渲染管道

从HTML文件传送到Blink到屏幕上显示像素的过程很长。渲染管道的架构如下。


blink_render_process.jpg
上一篇下一篇

猜你喜欢

热点阅读