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<SomeObject> {}
它使用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拥有一个DOMWindow
- Document相当于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的情况。
<!-- https://example.com -->
<body>
<iframe src="https://example2.com"></iframe>
</body>
如上例所示,对于主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<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
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<v8::Object> object = ...; // This is correct.
}
class SomeObject : public GarbageCollected<SomeObject> {
v8::Local<v8::Object> 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。