Electron 内部机制:消息循环集成
这是解释 Electron 内部原理的系列文章的第一篇。本文介绍了如何在 Electron 中将 Node 的事件循环与 Chromium 集成。
¥This is the first post of a series that explains the internals of Electron. This post introduces how Node's event loop is integrated with Chromium in Electron.
曾有多次尝试使用 Node 进行 GUI 编程,例如用于 GTK+ 绑定的 node-gui 和用于 QT 绑定的 node-qt。但它们都无法在生产环境中运行,因为 GUI 工具包有自己的消息循环,而 Node 使用 libuv 作为自己的事件循环,并且主线程一次只能运行一个循环。在 Node 中运行 GUI 消息循环的常用技巧是使用间隔非常短的计时器来触发消息循环,这会导致 GUI 界面响应缓慢并占用大量 CPU 资源。
¥There had been many attempts to use Node for GUI programming, like node-gui for GTK+ bindings, and node-qt for QT bindings. But none of them work in production because GUI toolkits have their own message loops while Node uses libuv for its own event loop, and the main thread can only run one loop at the same time. So the common trick to run GUI message loop in Node is to pump the message loop in a timer with very small interval, which makes GUI interface response slow and occupies lots of CPU resources.
在 Electron 的开发过程中,我们也遇到了同样的问题,尽管方式相反:我们必须将 Node 的事件循环集成到 Chromium 的消息循环中。
¥During the development of Electron we met the same problem, though in a reversed way: we had to integrate Node's event loop into Chromium's message loop.
主进程和渲染进程
¥The main process and renderer process
在深入探讨消息循环集成的细节之前,我将首先解释一下 Chromium 的多进程架构。
¥Before we dive into the details of message loop integration, I'll first explain the multi-process architecture of Chromium.
Electron 中有两种类型的进程:主进程和渲染进程(这实际上已经极其简化,完整视图请参见 多进程架构)。主进程负责 GUI 工作,例如创建窗口,而渲染进程仅负责运行和渲染网页。
¥In Electron there are two types of processes: the main process and the renderer process (this is actually extremely simplified, for a complete view please see Multi-process Architecture). The main process is responsible for GUI work like creating windows, while the renderer process only deals with running and rendering web pages.
Electron 允许使用 JavaScript 同时控制主进程和渲染进程,这意味着我们必须将 Node 集成到这两个进程中。
¥Electron allows using JavaScript to control both the main process and renderer process, which means we have to integrate Node into both processes.
用 libuv 替换 Chromium 的消息循环
¥Replacing Chromium's message loop with libuv
我的第一次尝试是使用 libuv 重新实现 Chromium 的消息循环。
¥My first try was reimplementing Chromium's message loop with libuv.
对于渲染器进程来说很容易,因为它的消息循环只监听文件描述符和计时器,我只需要使用 libuv 实现接口即可。
¥It was easy for the renderer process, since its message loop only listened to file descriptors and timers, and I only needed to implement the interface with libuv.
但是,这对于主进程来说要困难得多。每个平台都有自己的 GUI 消息循环。macOS Chromium 使用 NSRunLoop
,而 Linux 使用 glib。我尝试了很多方法,从原生 GUI 消息循环中提取底层文件描述符,然后将它们传递给 libuv 进行迭代,但仍然遇到了一些无效的极端情况。
¥However it was significantly more difficult for the main process. Each platform
has its own kind of GUI message loops. macOS Chromium uses NSRunLoop
,
whereas Linux uses glib. I tried lots of hacks to extract the
underlying file descriptors out of the native GUI message loops, and then fed
them to libuv for iteration, but I still met edge cases that did not work.
因此,我最终添加了一个计时器,用于在短时间间隔内轮询 GUI 消息循环。因此,该进程会持续占用 CPU,并且某些操作会长时间延迟。
¥So finally I added a timer to poll the GUI message loop in a small interval. As a result the process took a constant CPU usage, and certain operations had long delays.
在单独线程中轮询 Node 的事件循环
¥Polling Node's event loop in a separate thread
随着 libuv 的成熟,我们开始采用另一种方法。
¥As libuv matured, it was then possible to take another approach.
libuv 中引入了后端 fd 的概念,它是一个文件描述符(或句柄),libuv 会轮询它以进行事件循环。因此,通过轮询后端文件描述符 (fd),可以在 libuv 中出现新事件时收到通知。
¥The concept of backend fd was introduced into libuv, which is a file descriptor (or handle) that libuv polls for its event loop. So by polling the backend fd it is possible to get notified when there is a new event in libuv.
因此,我在 Electron 中创建了一个单独的线程来轮询后端文件描述符 (fd)。由于我使用系统调用进行轮询而不是使用 libuv API,因此它是线程安全的。每当 libuv 的事件循环中出现新事件时,都会向 Chromium 的消息循环发送一条消息,然后 libuv 的事件将在主线程中处理。
¥So in Electron I created a separate thread to poll the backend fd, and since I was using the system calls for polling instead of libuv APIs, it was thread safe. And whenever there was a new event in libuv's event loop, a message would be posted to Chromium's message loop, and the events of libuv would then be processed in the main thread.
通过这种方式,我避免了同时修补 Chromium 和 Node,并且主进程和渲染进程都使用了相同的代码。
¥In this way I avoided patching Chromium and Node, and the same code was used in both the main and renderer processes.
代码
¥The code
你可以在 electron/atom/common/
下的 node_bindings
文件中找到消息循环集成的实现。它可以轻松地复用到想要集成 Node 的项目。
¥You can find the implemention of the message loop integration in the
node_bindings
files under electron/atom/common/
. It can be
easily reused for projects that want to integrate Node.
更新:实现已移至 electron/shell/common/node_bindings.cc
。
¥Update: Implementation moved to electron/shell/common/node_bindings.cc
.