Electron 内部原理:消息循环集成
这是关于 Electron 内部机制系列文章的第一篇。本文将介绍 Node 的事件循环是如何集成到 Electron 的 Chromium 中的。
曾有许多使用 Node.js 进行 GUI 编程的尝试,例如用于 GTK+ 绑定的 node-gui,以及用于 QT 绑定的 node-qt。但它们都无法在生产环境中使用,因为 GUI 工具包有自己的消息循环,而 Node.js 使用 libuv 进行自己的事件循环,主线程一次只能运行一个循环。因此,在 Node.js 中运行 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.js 的事件循环
随着 libuv 的成熟,可以采用另一种方法。
libuv 中引入了 backend fd 的概念,它是一个文件描述符(或句柄),libuv 会轮询它来处理事件循环。因此,通过轮询 backend fd,可以在 libuv 有新事件时收到通知。
因此,在 Electron 中,我创建了一个单独的线程来轮询后端文件描述符,并且由于我使用的是系统调用进行轮询而不是 libuv API,因此它是线程安全的。每当 libuv 的事件循环中有新事件时,就会向 Chromium 的消息循环发布一条消息,然后 libuv 的事件将在主线程中进行处理。
通过这种方式,我避免了修改 Chromium 和 Node,并且主进程和渲染器进程使用了相同的代码。
代码
你可以在 electron/atom/common/
目录下的 node_bindings
文件中找到消息循环集成的实现。对于想要集成 Node.js 的项目来说,它可以很容易地被复用。
更新:实现已移至 electron/shell/common/node_bindings.cc
。