Electron 内部原理:消息循环集成
这是一个系列文章的第一篇,解释了 Electron 的内部机制。这篇文章介绍了 Node 的事件循环如何与 Electron 中的 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+ 绑定的 节点图形用户界面,以及用于 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 中,它是一个文件描述符(或句柄),libuv 会轮询它以进行事件循环。因此,通过轮询后台文件描述符,可以在 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 中,我创建了一个单独的线程来轮询后端文件描述符,并且由于我使用的是系统调用进行轮询而不是 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.