Blink是如何工作的

概述

Blink开发并不简单。对于Blink开发新手,不简单是因为,为了实现一个非常快的渲染引擎,已经引入了许多Blink特定的概念和编程约定。

即使对于经验丰富的 Blink 开发人员来说,这也并不容易,因为 Blink 非常庞大,而且对性能、内存和安全性极为敏感。

本文旨在从“3千米”高空视角来描述“Blink如何工作”,希望能够帮助Blink开发者快速熟悉Blink的架构:

  • 本文档不是关于Blink细节构成和编码规则(可能变动与过时)的手册、指南。相反,该文档简明地描述了 Blink 的基本原理,这些基础知识在短期内不太可能发生变化,并指出了如果您想了解更多信息可以阅读的资源。
  • 该文档不解释特定功能(例如,ServiceWorkers、编辑)。 相反,该文档解释了代码库中广泛(例如,内存管理、V8 API)使用的基本功能

Blink是做什么的?

  • Blink是Web Platform的渲染引擎。粗略地说,Blink完成了tab标签中渲染网页内容的所有事情,实现 Web 平台的规范(例如 HTML 标准),包括 DOM、CSS 和 Web IDL。
  • 嵌入V8并运行JavaScript
  • 从网络栈请求资源
  • 构建DOM树
  • 计算样式和排版
  • 嵌入Chrome合成器并绘制图形

许多“客户”(例如 Chromium、Android WebView 和 Opera)通过public content API 嵌入了 Blink。

image.png

从代码库的角度看,“Blink”通常指的是//third_party//blink/。从项目的角度来看,“Blink”通常是指实现web平台功能的项目。实现 Web 平台功能的代码主要分布在//third_party/blink/、//content/renderer/、//content/browser/ ,还有一些在其它地方。

进程和线程架构

进程

Chromium是多进程架构。它有一个browser进程和N个沙箱renderer进程。Blink运行在renderer进程。

会创建多少个renderer进程呢?出于安全考虑,隔离跨站点文档之间的内存地址区域(这叫做站点隔离 - Site Isolation)是很重要的。从概念上讲,一个renderer进程应该最多只服务于一个站点。然而,实际情况是,当用户打开大量标签页,或者设备内存不足时,限制一个站点一个renderer进程显得太繁重了。因此,不同站点的标签页或多个iframes可能共享一个renderer进程。这意味着一个标签页中的 iframe 可能由不同的renderer进程托管,而不同选项卡中的 iframe 可能由同一个renderer进程托管。也就是说,renderer 进程、标签页、iframes不存在1对1的关系

鉴于renderer进程在沙箱中运行,Blink需要通过Browser进程请求系统调用(例如:访问文件,播放音频)和访问用户资料数据(例如:cookie,密码)。Browser进程与renderer进程的进程间通信是通过Mojo实现的(以前是使用Chromium IPC,现在也还有许多地方是使用IPC。然而IPC是被废弃了的,新的代码都应该使用Mojo)。Chromium正在进行Servicification,即把browser进程抽象为一系列的服务。从Blink的角度,Blink可只使用Mojo同services和browser进程进行交互。

image.png

想要了解更多,可参考:

线程

Renderer进程中会创建多少个线程呢?

Blink有一个主线程,N个worker线程,还有几个内部线程。

几乎所有重要的事情都发生在主线程。所有的JavaScript(除了worker),DOM,CSS,样式和排版计算都运行在主线程。Blink高度优化以最大化主线程的性能。

Blink 可能会创建多个worker线程来运行 Web Workers、ServiceWorker 和 Worklets。

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

通过使用PostTask APIs传递消息来实现跨线程通信。不鼓励共享内存编程,除了出于性能原因确实需要使用它的几个地方。这就是为什么你在 Blink 代码库中看不到很多互斥锁的原因。

更多信息可参考:

Blink初始化和finalization

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

另一方面,Blink没有finalized,即renderer进程没有被清理就强制退出。原因之一是性能。另一个原因是通常很难以优雅有序的方式清理renderer进程中的所有内容(也没有必要这样做)。

目录结构

Content public APIs 和 Blink public APIs

Content public API是embedders能够embed渲染引擎的API层。需要小心维护Content public APIs,因为它是暴露给embedder的。

Blink public APIs是把third_party/blink的功能暴露给Chromium的API层。这个API层源自WebKit。在WebKit时代,Chromium和Safari共享WebKit的实现,因此API层需要把WebKit的能力暴露给Chromium和Safari。现在Chromium是third_party/blink的唯一的embedder,实际上这个API层不是必要的。现在我们正通过将web-platform代码迁移至blink(Onion Soup项目),来积极减少Blink public API的数量。

目录结构和依赖关系

third_party/blink有下面这些目录:

  • platform
    • Blink较底层的功能,从庞大的core/中分离出来的,例如:几何和图形实用库
  • core/ 和 modules
    • spec中定义的web平台功能的实现。core/实现DOM关联紧密的功能。module实现比较独立的功能,例如:webaudio,indexeddb
  • bindings/core,bindings/modules
    • 概念上,bindings/core是core/的一部分,bindings/module是module的一部分,大量使用V8 APIs的文件被放在bindings/{core, modules}中
  • controller
    • 一组使用/core,/module的上层库。例如devtools 前端

依赖顺序如下:

  • Chromium => controller => modules and bindins/modules => core and bindings/core =>
    platforms => 低级原语,如//base,//v8,//cc

关于目录结构的更详细信息可参考:blink/renderer/README.md

内存管理

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

  • PartionAlloc
  • Olipan(又名Blink GC)
  • malloc/free or new/delete

你可以使用USING_FAST_MALLOC在PartionAlloc 堆上分配一个对象:

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

应该使用scoped_refptr<> or std::unique_ptr<>来管理由PartionAllock分配的对象的生命周期。强烈建议不要手动管理这类对象的生命周期。Blink中禁止手动调用delete。

可以使用GarbageCollected在Olipan堆上分配一个对象:

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

通过Olipan分配的对象的生命周期由GC自动管理。必须使用特殊指针(例如,Member<>、Persistent<>)来保存 Oilpan 堆上的对象。查看Blink GC API reference以了解有关Olipan的编程限制。最重要的限制是,在Oilpan 对象的析构函数中,您不能访问任何其他Oilpan 对象(因为虚构的顺序是不保证的)

如果你既没使用USING_FAST_MALLOC,也没使用GarbageCollected,那么对象就是在系统的malloc堆上分配的。在Blink中是强烈不鼓励这样做的。所有的Blink对象都应该由PartionAlloc或Olipan来分配,规则如下:

  • 默认使用Olipan
  • 满足如下条件之一使用PartionAlloc
    • 1)对象的生命周期非常清晰,std::unique_ptr<> 或 scoped_refptr<> 就足够了
    • 2)在 Olipan 上分配对象会带来很多复杂性
    • 3)在 Olipan 上分配对象给垃圾收集运行时带来了很多不必要的压力

无论你使用的是PartionAlloc或是Olipan,都需要非常小心,避免创建悬空指针(注意:强烈建议不要使用裸指针)或内存泄露。

如果想要了解更多,可参考:

任务调度

为了改善渲染引擎的响应能力,Blink中的任务都应该尽可能的异步执行。应该避免使用同步IPC/Mojo,以及其他可能耗时几毫秒的操作(当然,有一些是无法避免的,像用户的JS执行)。

Renderer进程中的所有tasks都应该以正确的任务类型发布到Blink Schduler中。像这样:

// 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 正确和巧妙地安排任务非常重要。

更多细节可参考:如何发布任务

Web IDL bindings

Web IDL 是一种接口描述语言 (IDL) 格式,用于描述旨在在 Web 浏览器中实现的应用程序编程接口 (API)。其采用的动机是希望通过指定JavaScript等语言如何绑定到这些接口,来改善Web编程接口的互操作性。

当JavaScript访问node.firstChild时,node.h中的Node::firstChild被调用。怎么做到的呢?我们来瞧一瞧node.firstChild是个什么样的调用流程:

首先,你需要根据规范定义一个IDL文件:

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

Web IDL语法在WebIDL规范中定义。[...]叫做IDL扩展属性。一些IDL扩展属性在WebIDL规范中定义,其它一些是Blink特有的扩展属性。除了 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 绑定。自动生成的bindings位于

//src/out/{Debug,Release}/gen/third_party/ blink/renderer/bindings/core/v8/v8_node.h。当 JavaScript 调用 node.firstChild 时,V8 调用v8_node.h 中的 V8Node::firstChildAttributeGetterCallback(),然后调用C++中定义的的 Node::firstChild()。

想要了解更多,可参考:

Web IDL 编译器

IDL compiler或bindings生成器将Web IDL转换成C++代码,实现V8 与 Blink之间的绑定。也就是说,当从 JavaScript 使用 Web IDL 接口中的属性或方法时,V8 调用绑定代码,该代码调用 Blink 代码。

渲染流水线

从HTML文件发送到Blink到内容被渲染到屏幕上,是个很长的过程。渲染流水线架构如下:

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值