跳至主要內容

Electron 內部原理:訊息迴圈整合

·閱讀時間 3 分鐘

這是解釋 Electron 內部原理系列的第一篇文章。這篇文章介紹了如何在 Electron 中將 Node 的事件迴圈與 Chromium 整合。


過去有很多嘗試使用 Node 進行 GUI 程式設計的嘗試,例如用於 GTK+ 綁定的 node-gui,以及用於 QT 綁定的 node-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