[译] Blink内核是如何工做的?

原文连接css

Blink 是如何工做的

Author: haraken@
Last update: 2018 Aug 14
Status: PUBLIC
译: LeoYhtml

对于刚接触 Blink 的开发者来讲, Blink 相关的工做并不简单。由于实现一个高效快速的渲染引擎,须要了解大量与 Blink 相关的概念和代码约定。这对于经验丰富的 Blink 开发者来讲也并不简单,由于 Blink 项目很庞大,而且对于性能、内存和安全性很敏感。前端

本文的目标是提供一个关于 Blink 工做原理的概览,但愿可以帮助开发者快速熟悉 Blink 的架构。node

  • 本文不是一个关于 Blink 架构细节和代码风格的详细教程,而是关于 Blink 基本原理的简单介绍。这部分原理在短时间内不会有大的改变,另外提供了一些深刻了解这些部分的相关资源。
  • 本文不会介绍具体的功能(好比 ServiceWorkersediting 等),而是介绍了代码中普遍使用的一些基本的功能(好比内存管理, V8 APIs 等)

访问 Chromium wiki page 来获取更多的关于 Blink 开发的信息c++

Blink 作了什么

Blink 是一个Web平台的渲染引擎。粗略地说,在一个浏览器tab页中与内容渲染相关的全部事情都是由 Blink 实现的。git

  • 实现Web平台的规格(好比, HTML标准规格),包括 DOM , CSSWeb IDL (Web浏览器编程接口描述)github

  • 嵌入 V8 和运行 Javascriptweb

  • 从底层的网络堆栈请求资源数据库

  • 构建 DOM tree编程

  • 计算样式和布局

  • 嵌入 Chrome Compositor 和图形渲染绘制

    • what is Chrome Compositor?

    cc is responsible for taking painted inputs from its embedder, figuring out where and if they appear on screen, rasterizing and decoding and animating images from the painted input into gpu textures, and finally forwarding those textures on to the display compositor in the form of a compositor frame. cc also handles input forwarded from the browser process to handle pinch and scroll gestures responsively without involving Blink.

在不少地方都能见到 Blink 的身影,好比 ChromiumAndroid WebView 以及经过 content public APIs 内嵌 BlinkOpera 浏览器。

从代码库的角度来看, Blink 对应 //third_party/blink/ 。 从项目自己来看, Blink 实现了Web平台的功能,这些代码在主要在 //third_party/blink///content/renderer///content/browser/ 目录中。

进程/线程 架构

进程

Chromium 是一个 多进程架构 multi-process architecture 的浏览器引擎 。 Chromium 运行时会建立一个浏览器进程和N个在沙盒中运行的渲染进程。 Blink 则是在渲染进程中运行的。

建立多少渲染进程?通常来讲,一个 site 会独占一个渲染进程,而当用户开太多tabs页面内存不足时,多个 site 可能会共享一个渲染进程。

出于安全性的考虑,跨站文档(cross-site documents)的内存地址会被隔离开来(这被称为Site Isolation)。理想状况下,每一个渲染进程是每一个网站专用的,然而当用户打开太多标签页或者机器内存不够时,这种限制就很麻烦。因此实际上,多个页面或者不一样网站的多个 iframe 可能会共享同一个渲染。这意味着一个tab页中的多个 iframe 多是不一样的渲染进程渲染的,不一样的tab页中的 iframe 也有有多是同一个渲染进程渲染的。因此渲染进程,iframe 和 tab 三者之间不是(1:1)一对一的映射关系

因为渲染进程是运行在沙盒中的,因此 Blink 须要向浏览器进程发起系统调用(好比文件访问,音频播放)和(用户配置)数据的获取(好比 Cookie ,密码)。浏览器进程和渲染进程之间经过 Mojo 实现通讯。(Note: 之前是经过 Chromium IPC 实现,如今还有部分代码仍在使用,可是会逐渐弃用) Chromium 中的 Servicification 将浏览器进程封装出了一些独立的服务。 Blink 能够直接使用 Mojo 调用这些独立服务来或者与浏览器进程交互。

了解更多:

线程

在一个渲染进程中建立了多少线程?

Blink 中会有一个主线程,N个工做线程和三两个内部线程。

几乎全部重要的事情都发生在主线程中。 Javascript (不包括 service worker ), DOMCSS ,样式和布局计算都在主线程中运行。 Blink 经过许多优化来最大化主线程的性能,模拟了一个近乎单线程的架构。 Blink 可能会建立多个工做线程来运行 Web Workers ServiceWorker 以及 Worklet BlinkV8 可能会建立三两个内部进程来处理 音频 webaudio , 数据库 database , 内存回收 GC 等。

线程之间的通讯,须要使用 PostTask APIs 来传递。 出于性能的考虑,除了几个特别的地方,共享内存编程是不推荐的,因此在 Blink 中源码中也不多使用互斥锁这种东西。

了解更多:

Blink 的初始化和终止

Blink 经过 BlinkInitializer::Initialize() 来初始化。这个方法必须在执行 Blink 代码前调用。

Blink 没有终止化的状态,缘由是渲染进程是被强制退出的,而不是被清理回收的。缘由之一是出于性能的考虑(强制退出不须要作额外的操做)。另外一个缘由是渲染进程在正常退出的状况下,通常很难把因此东西都清理回收掉。(而且这样作的代价高于带来的效益)

目录架构

Content public APIsBlink public APIs

Content public APIs 是用嵌入渲染引擎的API层。 Content public APIs 必须当心维护,由于它们是提供给(想内嵌 Blink 引擎的)嵌入器的。

Blink public APIs 是为 Chromium 提供 //third_party/blink/ 中的 Blink 功能的API层。 Blink public APIs 继承自 Webkit APIs 。在 Webkit 时代,因为 ChromiumSafari 会共享 Webkit 的实现,因此当时这个API层既要顾及 Chromium 也要顾及 Safari 。而如今 Blink 内核的功能只须要提供给 Chromium ,旧的API层有些API就不须要了。因此咱们将 Chromium 中与平台相关的代码迁移到 Blink 中来减小 Blink public APIs 的数量(这个项目被叫做 Onion Soup

目录架构和依赖

//third_party/blink/ 的目录以下,查阅这个文档了解更多。

  • platform/
    • 一组从core/里面分解出来的 Blink 底层功能,好比地理位置 geometry 和图形 graphics 相关的库
  • core/ 和 modules/
    • 实现Web平台规格文件的全部功能。core/主要是实现 DOM 相关的功能。modules/主要实现了一些浏览器自有的功能,好比 webaudio , indexeddb
  • bindings/core/ 和 bindings/modules/
    • 从命名就能够猜到, bindings/core/core/ 的一部分, bindings/modules/modules/ 的一部分。频繁调用 V8 APIs 的文件都放在 bindings/{core,modules} 里面
  • controller/
    • 一些调用 core/modules/ 的顶层工具库(好比devtools的前端部分 devtools front-end

各部分代码的依赖关系以下:

  • Chromium => controller/ => modules/bindings/modules/ => core/bindings/core/ => platform/ => 底层原语 好比 //base , //v8//cc

提供给 //third_party/blink/ 底层原语在 Blink 项目中被当心翼翼地精心维护着

了解更多:

WTF

WTFBlink 特有的工具库,位于 platform/wtf/ 。咱们尽量统一 ChromiumBlink 的代码,因此 WTF 的体积会很小。 WTF 这个工具库之因此存在,是由于对于 Blink 的工做负载和 Oilpan (即 Blink GC ) 中有大量的类型( types ),容器( containers )和宏( macros )须要作性能优化。若是类型在 WTF 中有相应的定义,在 Blink 中就须要使用 WTF 的类型而不是定义在 //base 或者 std libraries 中的类型。 使用的最多的类型是 vectors , hashsets , hashmapsstrings 。相应的在 Blink 中应该使用 WTF::Vector , WTF::HashSet , WTF::HashMap , WTF::StringWTF::AtomicString 而不是 std::vector , std::*set, std::*mapstd::string

了解更多:

内存管理

你须要关注三个与 Blink 相关的内存分配器。

给一个对象分配 PartitionAlloc 上的堆内存,能够用 USING_FAST_MALLOC()

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 是不容许的。

给一个对象分配 Oilpan 上的堆内存,你可使用 GarbageCollected

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

Oilpan 堆内存中的对象生命周期是由 garbage collection 自动管理的。你须要使用特殊的指针(好比 Member<>Persistent<> )来存 Oilpan 堆内存中的对象。参考这个API手册this API reference来熟悉在 Oilpan 上开发的一些限制。最重要的一个限制就是在一个 Oilpan 对象的解构函数中不容许处理任何其余的 Oilpan 对象。(缘由是解构的顺序是没有保证的)

若是你既没有使用 USING_FAST_MALLOC() 也没有使用 GarbageCollected ,那么对象就被分配在系统堆内存中。这在 Blink 中是极其不推荐的。全部的 Blink 对象都应该按以下规则分配在 PartitionAlloc 或者 Oilpan 的堆内存中。

  • 默认使用 Oilpan
  • 仅在三种状况下使用 PartitionAlloc
    • 对象的生命周期很是清晰,只用 std::unique_ptr<>scoped_refptr<> 就能够知足需求
    • 当使用 Oilpan 分配内存给当前状况增长不少的复杂性时
    • 当使用 Oilpan 分配内存给当前状况的垃圾回收机制 garbage collection runtime 带来了大量没必要要的(性能)压力时

无论使用 PartitionAlloc 仍是 Oilpan 来分配内存,都须要极其当心,以避免建立出悬空指针( Dangling pointer )甚至内存泄露(Note: 裸指针也是极其其不推荐的)

了解更多:

任务调度

为了提高渲染引擎的响应速度,在 Blink 中的任务都应该尽量的异步执行。同步的 IPC/Mojo 或者其余可能耗费数毫秒的操做都是不推荐使用的。(尽管有些操做没法避免,好比执行用户的 JavaScript ,其 JavaScript 代码自己可能会阻塞渲染)。

在渲染进程中的全部任务都会通知 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 Schedule 的高效调度是十分重要的。

了解更多:

  • How to post tasks 如何发起任务: platform/scheduler/PostTask. md

Page , Frame , Document , DOMWindow etc

概念

Page , Frame , Document , ExecutionContextDOMWindow 的含义以下:

  • Page 对应tab页(若是下文介绍的 OOPIF 没有开启的话)。一个渲染进程可能会渲染多个tab页
  • Frame 对应 frame ( main frame 或者一个 iframe )。一个 Page 包含一个或者多个 Frame 而且包含在一个树结构中。
  • DOMWindow 对应 Javascript 中的 window 对象。每一个 Frame 有一个 DOMWindow
  • Document 对应 Javascript 中的 window.document 对象。每一个 Frame 有一个 Document
  • ExecutionContext 是(主线程的) Document 和(工做线程的) WorkerGlobalScope 的抽象。

渲染进程 : 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. 建立了新的 DOMWindowDocument ,而 Frame 则可能被重用。(Note: 准确的来讲,还存在一些更复杂的状况建立了新的 Document ,而 DOMWindowFrame 被重用了)。

了解更多:

  • core/frame/FrameLifecycle. md

Out-of-Process iframes (OOPIF 进程外的iframe)

Site Isolation 网站隔离增长了安全性的同时也增长了复杂性。:) Site Isolation 的设想是为每个网站建立一个渲染进程 。

(A site is a page’s registrable domain + 1 label, and its URL scheme. For example, mail.example.com and chat.example.com are in the same site, but noodles.com and pumpkins.com are not. )

若是 Page 包含跨站的 iframe , 那么这个 Page 可能被两个渲染进程共同渲染。参考下面这种 Page :

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

frameiframe 可能运行在不一样的渲染进程中。渲染进程本地的 frameLocalFrame 呈现,不属于渲染进程本地的 frameRemoteFrame 呈现。

从主 frame 的角度来看,主 frame 是一个 LocalFrameiframe 是一个 RemoteFrame 。从 iframe 的角度来看,主 frame 是一个 RemoteFrame ,而 iframe 是一个 LocalFrame

LocalFrameRemoteFrame (二者可能存在与不一样的渲染进程中)之间的通讯是浏览器进程来处理的。

了解更多:

Detached Frame / Document 分离的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.
复制代码

一个很骚的事实是,在分离的 frame 中你仍可以运行脚本和执行DOM操做。因为 frame 已经被分离,大部分DOM操做会失败并报错。惋惜分离的 frame 的表如今不一样浏览器中并不一致,在规格文件中也没有很是明确的定义。大致上来讲,指望的表现是在 frame 分离后 JavaScript 仍是能够正常的执行,可是大多数DOM操做都应该失败并抛出异常,如:

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

这意味着 Blink 须要在 frame 被分离时作大量的清除回收操做。这些操做能够经过 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. } }; 复制代码

Web IDL bindings: Web IDL绑定

JavaScript 访问 node.firstChild 的时候, node.h 中的 Node::firstChild() 即被调用。它是如何工做的呢,一块儿来看看 node.firstChild 是怎么工做的:

首先,你须要为每个规格定义一个IDL文件,如:

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

Web IDL的语法定义在 the Web IDL spec 中。 [...] 被称为 IDL extended attributesthe Web IDL spec 里面定义了一些 IDL extended attributes ,其余的在 Blink-specific IDL extended attributes中。除了 Blink 特有的 IDL extended attributes ,其余IDL文件都应该按照和规格文件一致的格式来写(意思就是直接从规格文件里面cv)。

接下来,你须要为 Node 节点定义一个 C++ class 类,并用c++实现 firstChildgetter ,如:

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 the IDL compiler 会为 Node interfaceNode.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()

了解更多:

  • How to add Web IDL bindings: bindings/IDLCompiler. md
  • How to use IDL extended attributes: bindings/- IDLExtendedAttributes. md
  • Spec: Web IDL spec

V8 和 Blink

Isolate, Context, World

当你写和 V8 APIs 有关的代码时,理解 Isolate, Context, World 这三个概念很重要。它们在代码库中分别是 v8::Isolate , v8::ContextDOMWrapperWorld

Isolate 是一个物理上的线程,在 BlinkIsolate : physical=1:1 。主线程和工做线程都有其独立的 Isolate

Context 是一个全局的对象(以 Frame 来讲, FrameContextwindow 对象)。因为每一个 frame 有本身的 window 对象,因此一个渲染进程中会有多个 Context 。当调用 V8 APIs 时,你须要确认你在正确的 Context 中。不然, v8::Isolate::GetCurrentContext() 就会返回一个不正确的 Context ,最坏的状况会形成对象泄露并致使安全问题。

World 支撑 Chrome extensions 脚本的运行。 Worlds 和任何web标准都没有关系。 Chrome extensions 脚本和页面共享DOM,不过处于安全的考虑, Chrome extensions 脚本的 JavaScript 和页面的 JavaScript 堆内存是相互隔离的。(而且 Chrome extensions 脚本之间的 JavaScript 堆内存也是相互隔离的)。主线程经过为页面建立一个 main world 和为每一个 Chrome extensions 脚本建立一个 isolated world 来实现隔离。 main worldisolated worlds 均可以访问到C++上的DOM对象,可是他们各自的 JavaScript 对象都是隔离的。这种隔离是经过为每一个 C++DOM 对象建立多个 V8 wrapper 来实现的。即每一个 world 对应一个 V8 wrapper

Context , WorldFrame 之间有什么联系? 想象一下, 在主线程中存在N个 World (一个 main world +(N-1)个 isolated worlds) )。那么一个 Frame 就有N个 window objects ,每一个 window objects 对应一个 world 。而 Context 也是对应 window objects ,这意味着当存在N个 Frame 和N个 Worlds 的时候,有M*N个 Contexts (不过 Contexts 是懒加载建立的)

对于 worker 而言,只有一个 World 和一个 global object ,因此就只存在一个 Context

此外,当你使用 V8 APIs 的时候,你应该很是注意是否使用了正确的 context ,不然你可能致使在不一样的 isolated worlds 间泄露 JavaScript 对象甚至致使灾难般的安全问题。(好比,使得A. com的 Chrome extetion 能够操纵B. com的 Chrome extetion

了解更多:

V8 APIs

//v8/include/v8. h. 里面有大量的V8 APIs。因为 V8 APIs 都比较底层,使用起来略显麻烦,因此通常使用platform/bindings/ 提供的一组封装了的 V8 APIs 辅助类来( helper classes )进行调用。你应该尽可能使用 helper classes 。若是你的代码中会重度使用原生 V8 APIs ,这些代码应该放到 bindings/{core,modules} 里面去。

V8使用 handle 来指向 V8 objects 。最多见的 handlev8::Local<> , v8::Local<> 用于从机器堆栈 machine stack 指向 V8 objectsv8::Local<> 必须在 v8::HandleScope 从机器堆栈 machine stack 分配以后才能使用。 v8::Local<> 也不能在 machine stack 以外使用:

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.
};
复制代码

要从机器堆栈 machine stack 指外指向 V8 objects ,你须要使用 wrapper tracing。然而你须要特别当心地使用,以避免建立出循环引用。一般 V8 APIs 都是难用的。若是你不肯定你的用法能够上blink-review-bindings@提问。

了解更多:

  • How to use V8 APIs and helper classes: platform/bindings/HowToUseV8FromBlink. md

V8 wrappers

每一个 C++ DOM 对象(好比,Node节点)都有其对应的 V8 wrapper 。准确的说,每一个 world 的每一个 C++ DOM 对象都有其对应的 V8 wrapper

V8 wrappers 对它相应的 C++ DOM 是强引用关系。而 C++ DOMV8 wrappers 则是弱引用关系。因此若是想要使 V8 wrappers 在一段特定的周期延续,你须要明确地指定。不然 V8 wrappers 可能会被提早回收,致使 V8 wrappers 上的 JS properties 丢失...

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. 复制代码

若是什么都不作的话, child 就会被 GC 回收,即 child.foo 就不存在了。要保留 div.firstChild 上的 V8 wrapper 的话,咱们须要增长一个机制来实现:只要 div 所属的 DOM tree 还能够经过 V8 访问到,就一直保留 div.firstChild 上的 V8 wrapper

有两种方式来保留 V8 wrappersActiveScriptWrappablewrapper tracing.

了解更多:

渲染管道 Rendering pipeline

一个HTML文件从传递到 Blink 再到屏幕上显示的像素之间有一段很长的历程。渲染管道的架构以下:

Life of A Pixel里面介绍了渲染管道的每个阶段。

了解更多:

Questions?

有问题能够到 blink-dev@chromium.orgplatform-architecture-dev@chromium 提问。

相关文章
相关标签/搜索