Blink学习第一天:How Blink works
博格巴世界杯 2103 2025-08-22 12:40:31

Blink

Blink是Chromium的浏览器引擎 详见Blink官网 How Blink works是官网对它的介绍文档。本帖是对该文档的中文总结。

Blink的主要功能

实现web平台的各种特性(比如:HTML标准),包括DOM,CSS和Web IDL嵌入V8引擎(一种Javascript引擎)并运行JS代码向底层的网络栈请求资源建立DOM树推断出样式和布局的信息嵌入Chrome Compositor

Chromium、Opera、Android的WebView都在使用Blink提供的服务。它们的层级关系如下图所示: 另外,Blink的代码处于third_party/blink文件夹之中。

进程

众所周知,Chromium属于多进程架构,它含有一个browser进程和多个renderer进程,而且由于安全原因,这些renderer进程都置于一个sanbox之中。而Blink运行在一个renderer进程之中。

严格地说,一个标签页应当对应一个renderer进程。但若用户打开的标签页过多,进程之间的资源分配不得不被慎重考虑。因此,一个renderer进程有时可以对应多个frames或多个标签页。原文在这部分的最后总结得十分清楚:

There is no 1:1 mapping between renderer processes, iframes and tabs.

通信方面,当Blink使用mojo与browser进程进行通信。通信的内容主要包括系统调用(比如访问本地文件、播放视频)以及获取cookies和用户密码。现今,由于browser进程的代码正在转向面向服务的设计方法,所以Blink有时也会与这些broswer进程提供的Service进行通信。

线程

Blink由一个主线程和多个内在线程组成。所有的JS代码(除了worker)、DOM、CSS、样式布局推断都在主线程进程。放心,主线程已被最大程度地优化,它可以胜任这一切。

Blink还会创建一些新的线程比如Web Worker、Service Worker和Worklets。而当它与V8协同工作时,它还可以创建线程去播放视频(webaudio),管理数据库以及进行垃圾回收。

Blink使用PostTaskAPI进行跨线程通信。Blink极少使用共享内存进行线程通信。

Blink启动和终止

启动时通过BlinkInitializer::Initialize()进行初始化。而由于对性能方面的考虑,它从不进行清理工作而是直接退出。

文件结构

首先是两个概念: Content public API: 便于需要嵌入浏览器引擎的使用者进行嵌入工作的API Blink public API: 历史遗留的API,开发者正在逐步减少这一API,这个工作的命名很有意思,叫洋葱汤(Onion Soup)。

接着是各个模块的大致介绍: platform/: 底层实现 core/ , modules/: web平台特性的实现,比如webaudio,indexeddb bingdings/core/ , bindings/modules/: V8 API的嵌入 controller/: 一些更高水平的模块,比如F12 顺带一提,比platform更底层的模块有//base,//v8,//cc

WTF

一个blink专用的数据结构。出场最多的莫过于WTF::Vector, WTF::HashSet, WTF::HashMap, WTF::String以及WTF::AtomicString。之所以使用专用的数据结构,是因为blink为这些数据结构设置了专门的垃圾回收机制。

内存管理

Blink有且仅有两种内存管理方式,没有malloc/free以及new/delete。 第一种:PartitionAlloc

class SomeObject {

USING_FAST_MALLOC(SomeObject);

}

它的底层是C++11的独占指针std::unique_ptr以及chromium的scoped_refptr<>。

第二种:Oilpan

class SomeObject : public GarbageCollected {}

它使用Oilpan堆进行垃圾回收管理。

需要注意的是,Blink默认选择第二种进行内存管理。当对象生命周期十分清楚,且使用Oilpan过于复杂、回收工作的压力大时,才可以选择第一种。

任务调度

为了提高响应能力,blink的任务被尽量设计为异步执行,尽管有些任务不可避免是同步执行的(比如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则通过管理多个任务队列来完成调度任务。

Page, Frame, Document, ExecutionContext and DOMWindow

Page相当于标签页,一个renderer进程可能管理多个标签页Frame相当于主frame或iframe,Page和Frame的关系通过树(一种数据结构)来体现DOMWindow相当于Javascript的window对象,一个Frame拥有一个DOMWindowDocument相当于Javascript的window.document对象,因此,一个Frame也拥有一个Document对象ExecutionContext是主线程的Document和worker线程的WorkerGlobalScope两者的抽象体现。

关于它们的关系,原文写的十分清楚:

Renderer process : Page = 1 : N. Page : Frame = 1 : M. Frame : DOMWindow : Document (or ExecutionContext) = 1 : 1 : 1

有时,上面一行的关系会发生变化,比如下面这行代码:

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

此时,Frame会被重用,而DOMWindow和Document会被重新创建。

OOPIF (Out-of-process Frame)

基于浏览器的安全机制,如果一个Page里面存在两个Frame不同域,那么Blink可能就会创建两个renderer进程,也就是说,可能存在两个renderer进程管理同一个Page的情况。

如上例所示,对于主frame而言,https://example.com视为LocalFrame,而https://example2.com则视为RemoteFrame。而对于iframe而言,情况相反。但无论如何,LocalFrame和RemoteFrame的通信都是通过browser进程实现的,因为它们属于不同的renderer进程。

分离的Frame和Document

doc = iframe.contentDocument;

// The iframe is detached from the DOM tree.

iframe.remove();

// But you still can run scripts on the detached frame.

doc.createElement("div");

你可以如上这样做,若如此做了,大部分DOM操作函数会报错,但仍有不完善的地方。因为DOM操作函数会如下面代码那样检查:

void someDOMOperation(...) {

if (!script_state_->ContextIsValid()) { // The frame is already detached

…; // Set an exception etc

return;

}

}

但此时,ContextIsValid的判断条件的设置将变得十分困难,因为垃圾回收工作十分庞大。下面代码是介绍如何进行这种情况的垃圾回收:

class SomeObject

: public GarbageCollected,

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

IDL实际上是建立了Javascript与C++的一座桥梁。 原文是以javascript的node.fristChild和node.h的Node::firstChild为例。

首先,在node.idl文件中定义一个映射。

// node.idl

interface Node : EventTarget {

[...] readonly attribute Node? firstChild;

};

然后,在C++文件node.h中定义映射结果。

// 所有暴露给javascript的接口都需要继承ScriptWrappable

class EventTarget : public ScriptWrappable {

...;

};

class Node : public EventTarget {

// 所有需要进行映射的类都需要下面这行宏

DEFINE_WRAPPERTYPEINFO();

Node* firstChild() const { return first_child_; }

};

需要注意的是,C++文件和IDL文件需同名。

此时,IDL编译器会在//src/out/{Debug,Release}/gen/third_party/ blink/renderer/bindings/core/v8/v8_node.h自动生成绑定。最后的流程是:

Isolate, Context, World

这些是V8的一些概念。 Isolate相当于物理线程。主线程有自己的Isolate,worker线程有自己的Isolate。 Context相当于一个全局对象。对于主线程而言,Context就是Page的一个window对象。而Page可以有多个window对象,所以使用v8的API时需要注意是否访问到正确的对象。 World是一种视角,是专门针对插件的安全性而定义的概念。每一个插件都可以访问DOM树,但为了安全,主线程会对每一个插件安排一个世界,每一个插件都在自己的世界里访问DOM树。在代码中,每一个世界就是一个V8Wrapper对象。 总的来说,对于主线程而言,一个Page有许多Frame,因此有很多window对象,而一个window对象可以有多个世界访问它。所以我们可以有M*N个Context(M个Frame和N个world)。当然,Context是懒加载的,且M和N的数字一般很小。

V8的APIs

V8的API存放在//v8/include/v8.h里,但Chromium在platform/bindings/中提供了一些封装v8的类以便于更准确地进行使用。

V8使用两种方式操作V8对象:

当对象处于machine stack中时,先创建HandleScope对象,再使用v8::Local<>操作v8对象。(下面代码是这种情况的一个例子)当对象不处于machine stack时,应当使用wrapper track,这种方式十分容易导致引用循环。

void function() {

v8::HandleScope scope;

v8::Local object = ...; // This is correct.

}

class SomeObject : public GarbageCollected {

v8::Local object_; // This is wrong.

};

V8的Wrapper

上文提到,每一个DOM对象在每一个世界都存在一个V8Wrapper。但值得注意的是,V8Wrapper有DOM对象的强引用,但DOM对象只有V8Wrapper的弱引用。这有什么问题呢?我们来看看下面的代码。

div = document.getElementbyId("div");

child = div.firstChild;

child.foo = "bar";

child = null;

// 如果啥也不做,child就会被GC

gc();

// 然后下面这行就失效了

assert(div.firstChild.foo === "bar");

因为child被置为null了,所以child很容易被GC,而此时child是V8Wrapper,它指向的是DOM对象,即某一节点的firstChild,由于child是强引用,GC之后div.firstChild也没了,所以div.firstChild,foo也就没了。 若希望V8Wrapper不被GC的话,有两种方法:ActiveScriptWrappable和wrapper tracing。

渲染过程

Copyright © 2022 98世界杯_乌拉圭世界杯 - cy078.com All Rights Reserved.