跳至主要内容

Electron 内部机制:消息循环集成

·阅读时间:3 分钟

这是一系列文章的第一篇,解释了 Electron 的内部机制。这篇文章介绍了 Node 的事件循环是如何在 Electron 中与 Chromium 集成的。


曾经有很多尝试使用 Node 进行 GUI 编程,比如 node-gui 用于 GTK+ 绑定,以及 node-qt 用于 QT 绑定。但它们都没有在生产环境中运行,因为 GUI 工具包有自己的消息循环,而 Node 使用 libuv 来进行自己的事件循环,并且主线程一次只能运行一个循环。因此,在 Node 中运行 GUI 消息循环的常用技巧是使用非常小的间隔时间在计时器中泵送消息循环,这会使 GUI 界面响应速度变慢并占用大量 CPU 资源。

在开发 Electron 的过程中,我们遇到了同样的问题,尽管方向相反:我们必须将 Node 的事件循环集成到 Chromium 的消息循环中。

主进程和渲染进程

在我们深入研究消息循环集成的细节之前,我将首先解释 Chromium 的多进程架构。

在 Electron 中,有两种类型的进程:主进程和渲染进程(实际上这只是极度简化的描述,请参阅 多进程架构 了解完整的视图)。主进程负责 GUI 工作,比如创建窗口,而渲染进程只处理运行和渲染网页。

Electron 允许使用 JavaScript 来控制主进程和渲染进程,这意味着我们必须将 Node 集成到这两个进程中。

使用 libuv 替换 Chromium 的消息循环

我第一次尝试是用 libuv 重新实现 Chromium 的消息循环。

对于渲染进程来说,这很容易,因为它的消息循环只监听文件描述符和计时器,我只需要实现与 libuv 的接口。

然而,对于主进程来说,这要困难得多。每个平台都有自己的 GUI 消息循环类型。macOS Chromium 使用 NSRunLoop,而 Linux 使用 glib。我尝试了许多技巧来从原生 GUI 消息循环中提取底层文件描述符,然后将它们提供给 libuv 进行迭代,但我仍然遇到了一些无法正常工作的边缘情况。

所以最后我添加了一个计时器,以很小的间隔轮询 GUI 消息循环。结果是进程占用恒定的 CPU 使用率,并且某些操作有很长的延迟。

在单独的线程中轮询 Node 的事件循环

随着 libuv 的成熟,现在可以采用另一种方法。

libuv 中引入了后端 fd 的概念,它是一个文件描述符(或句柄),libuv 会对其进行轮询以进行事件循环。因此,通过轮询后端 fd,可以获取 libuv 中存在新事件时的通知。

所以在 Electron 中,我创建了一个单独的线程来轮询后端 fd,并且由于我使用系统调用进行轮询而不是 libuv API,所以它是线程安全的。并且每当 libuv 的事件循环中出现新事件时,就会向 Chromium 的消息循环发布一条消息,然后将在主线程中处理 libuv 的事件。

通过这种方式,我避免了修补 Chromium 和 Node,并且相同的代码在主进程和渲染进程中都使用。

代码

你可以在 electron/atom/common/ 下的 node_bindings 文件中找到消息循环集成的实现。它可以轻松地重复用于想要集成 Node 的项目。

更新:实现已移至 electron/shell/common/node_bindings.cc