Blink学习第一天:How Blink works

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与其它层级的关系
另外,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通信
 
 

线程

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对象。
V8与Blink
总的来说,对于主线程而言,一个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。
 
 

渲染过程

渲染过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值