Electron 内部原理:将 Chromium 构建为库
Electron 基于 Google 开源的 Chromium 项目,而 Chromium 项目本身并非专门为其他项目设计。这篇文章将介绍 Chromium 如何被构建成一个库供 Electron 使用,以及构建系统在多年间的演变。
使用 CEF
Chromium Embedded Framework (CEF) 是一个将 Chromium 转换为库的项目,并基于 Chromium 代码库提供稳定的 API。Atom 编辑器和 NW.js 的早期版本都使用了 CEF。
为了维护稳定的 API,CEF 隐藏了 Chromium 的所有细节,并用自己的接口封装了 Chromium 的 API。因此,当我们想访问底层的 Chromium API,比如将 Node.js 集成到网页中时,CEF 的优势反而成了障碍。
最终,Electron 和 NW.js 都转而直接使用 Chromium 的 API。
作为 Chromium 的一部分进行构建
尽管 Chromium 官方并不支持外部项目,但其代码库是模块化的,很容易基于 Chromium 构建一个最小化的浏览器。提供浏览器界面的核心模块称为 Content Module。
要开发使用 Content Module 的项目,最简单的方法是将项目作为 Chromium 的一部分进行构建。这可以通过先检出 Chromium 的源代码,然后将项目添加到 Chromium 的 DEPS
文件中来完成。
NW.js 和 Electron 的早期版本都采用了这种构建方式。
缺点是 Chromium 是一个非常庞大的代码库,需要非常强大的机器才能构建。对于普通的笔记本电脑,这可能需要 5 个多小时。这极大地影响了能够为项目贡献的开发者数量,也减慢了开发速度。
将 Chromium 构建为单个共享库
作为 Content Module 的用户,Electron 在大多数情况下不需要修改 Chromium 的代码,因此改进 Electron 构建的一个显而易见的方法是将 Chromium 构建为共享库,然后在 Electron 中与其链接。这样,开发者在为 Electron 做出贡献时就不再需要构建整个 Chromium。
libchromiumcontent
项目是由 @aroben 为此目的而创建的。它将 Chromium 的 Content Module 构建成一个共享库,然后提供 Chromium 的头文件和预构建的二进制文件供下载。libchromiumcontent 初始版本的代码可以在 此链接 中找到。
brightray
项目也是作为 libchromiumcontent 的一部分诞生的,它在 Content Module 周围提供了一个精简的封装。
通过结合使用 libchromiumcontent 和 brightray,开发者无需深入了解 Chromium 的构建细节即可快速构建浏览器。并且消除了构建项目对快速网络和强大机器的要求。
除了 Electron 之外,还有其他 Chromium 驱动的项目也采用了这种方式构建,例如 Breach 浏览器。
过滤导出的符号
在 Windows 上,一个共享库可以导出的符号数量是有限制的。随着 Chromium 代码库的增长,libchromiumcontent 中导出的符号数量很快就超过了限制。
解决方法是在生成 DLL 文件时过滤掉不需要的符号。这可以通过 向链接器提供一个 .def
文件,然后使用一个脚本来 判断命名空间下的符号是否应该被导出 来实现。
通过这种方法,即使 Chromium 不断添加新的导出符号,libchromiumcontent 仍然可以通过剥离更多符号来生成共享库文件。
组件构建
在介绍 libchromiumcontent 的下一步之前,首先介绍 Chromium 中的组件构建概念非常重要。
作为一个庞大的项目,Chromium 在构建时的链接步骤非常耗时。通常,当开发者进行一个小改动时,可能需要 10 分钟才能看到最终输出。为了解决这个问题,Chromium 引入了组件构建,它将 Chromium 中的每个模块构建成独立的共享库,这样最终链接步骤花费的时间就变得微不足道了。
分发原始二进制文件
随着 Chromium 的不断发展,Chromium 中导出的符号越来越多,即使是 Content Module 和 Webkit 的符号也超过了限制。仅仅通过剥离符号已经不可能生成可用的共享库了。
最终,我们不得不 分发 Chromium 的原始二进制文件,而不是生成单个共享库。
如前所述,Chromium 有两种构建模式。由于分发原始二进制文件,我们不得不在 libchromiumcontent 中分发两种不同的二进制文件。一种称为 static_library
构建,它包含 Chromium 正常构建生成的每个模块的所有静态库。另一种是 shared_library
,它包含 Chromium 组件构建生成的每个模块的所有共享库。
在 Electron 中,Debug 版本链接的是 libchromiumcontent 的 shared_library
版本,因为它下载量小,并且在链接最终可执行文件时花费时间少。而 Electron 的 Release 版本则链接 libchromiumcontent 的 static_library
版本,这样编译器就可以生成完整的符号,这对于调试很重要,并且链接器可以进行更好的优化,因为它知道哪些目标文件是必需的,哪些不是。
因此,对于正常开发,开发者只需要构建 Debug 版本,这不需要好的网络或强大的机器。虽然 Release 版本构建需要更好的硬件,但它可以生成更好的优化后的二进制文件。
gn
更新
作为世界上最大项目之一,大多数普通系统都不适合构建 Chromium,而 Chromium 团队开发了自己的构建工具。
Chromium 的早期版本使用 gyp
作为构建系统,但它存在速度慢的问题,并且其配置文件对于复杂项目来说变得难以理解。经过多年的发展,Chromium 切换到了 gn
作为构建系统,它速度更快,并且拥有清晰的架构。
gn
的一个改进是引入了 source_set
,它代表一组目标文件。在 gyp
中,每个模块要么由 static_library
或 shared_library
表示,对于 Chromium 的正常构建,每个模块生成一个静态库,并在最终可执行文件中链接在一起。通过使用 gn
,每个模块现在只生成一组目标文件,最终的可执行文件只是将所有目标文件链接在一起,因此不再生成中间静态库文件。
然而,这个改进给 libchromiumcontent 带来了很大的麻烦,因为 libchromiumcontent 实际上需要中间静态库文件。
第一次尝试解决这个问题是 修补 gn
以生成静态库文件,这解决了问题,但远非一个体面的解决方案。
第二次尝试是由 @alespergl 完成的,他 从目标文件列表中生成自定义静态库。它使用了这样一个技巧:首先运行一个虚拟构建来收集生成的目标文件列表,然后通过将列表提供给 gn
来实际构建静态库。它只对 Chromium 的源代码进行了最小的修改,并保持了 Electron 的构建架构。
总结
正如你所看到的,与将 Electron 构建为 Chromium 的一部分相比,将 Chromium 构建为库需要更多的努力和持续的维护。然而,后者消除了构建 Electron 对强大硬件的要求,从而使更广泛的开发者能够构建和贡献 Electron。这项努力是完全值得的。