Electron 內部原理:弱參考
作為一種具有垃圾回收的語言,JavaScript 將使用者從手動管理資源中解放出來。但是由於 Electron 託管了這個環境,因此它必須非常小心地避免記憶體和資源洩漏。
這篇文章介紹了弱參考的概念,以及它們在 Electron 中如何用於管理資源。
弱參考
在 JavaScript 中,每當您將物件指派給變數時,您都在為該物件新增一個參考。只要存在對該物件的參考,它就會一直保留在記憶體中。一旦對該物件的所有參考都消失了,也就是說,不再有變數儲存該物件,JavaScript 引擎就會在下次垃圾回收時回收記憶體。
弱參考是對物件的參考,它允許您取得物件,而不會影響它是否會被垃圾回收。當物件被垃圾回收時,您也會收到通知。這樣就有可能使用 JavaScript 管理資源。
以 Electron 中的 NativeImage
類別為例,每次您呼叫 nativeImage.create()
API 時,都會傳回一個 NativeImage
實例,並且它將圖像資料儲存在 C++ 中。一旦您完成了該實例,並且 JavaScript 引擎 (V8) 已經垃圾回收了該物件,就會呼叫 C++ 中的程式碼來釋放記憶體中的圖像資料,因此使用者無需手動管理。
另一個例子是視窗消失問題,它直觀地展示了當對視窗的所有參考都消失時,視窗是如何被垃圾回收的。
在 Electron 中測試弱參考
由於 JavaScript 語言沒有指派弱參考的方法,因此無法在原始 JavaScript 中直接測試弱參考。JavaScript 中唯一與弱參考相關的 API 是 WeakMap,但由於它只建立弱參考鍵,因此無法知道物件何時被垃圾回收。
在 v0.37.8 之前的 Electron 版本中,您可以使用內部的 v8Util.setDestructor
API 來測試弱參考,它會為傳遞的物件新增一個弱參考,並在物件被垃圾回收時呼叫回呼
// Code below can only run on Electron < v0.37.8.
var v8Util = process.atomBinding('v8_util');
var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});
// Remove all references to the object.
object = undefined;
// Manually starts a GC.
gc();
// Console prints "The object is garbage collected".
請注意,您必須使用 --js-flags="--expose_gc"
命令開關啟動 Electron,以公開內部的 gc
函數。
該 API 在後續版本中被移除,因為 V8 實際上不允許在解構函式中執行 JavaScript 程式碼,並且在後續版本中這樣做會導致隨機崩潰。
remote
模組中的弱參考
除了使用 C++ 管理原生資源外,Electron 還需要弱參考來管理 JavaScript 資源。一個例子是 Electron 的 remote
模組,它是一個遠端程序呼叫 (RPC) 模組,允許從渲染器進程中使用主進程中的物件。
remote
模組的一個關鍵挑戰是避免記憶體洩漏。當使用者在渲染器進程中取得遠端物件時,remote
模組必須保證該物件在主進程中繼續存在,直到渲染器進程中的參考消失。此外,它還必須確保當渲染器進程中不再有對該物件的任何參考時,該物件可以被垃圾回收。
例如,在沒有正確實作的情況下,以下程式碼會快速導致記憶體洩漏
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}
remote
模組中的資源管理很簡單。每當請求一個物件時,就會向主進程發送一條訊息,Electron 會將該物件儲存在一個映射表中並為其指派一個 ID,然後將該 ID 發送回渲染器進程。在渲染器進程中,remote
模組將接收 ID 並使用代理物件包裝它,當代理物件被垃圾回收時,將向主進程發送一條訊息以釋放該物件。
以 remote.require
API 為例,一個簡化的實作看起來像這樣
remote.require = function (name) {
// Tell the main process to return the metadata of the module.
const meta = ipcRenderer.sendSync('REQUIRE', name);
// Create a proxy object.
const object = metaToValue(meta);
// Tell the main process to free the object when the proxy object is garbage
// collected.
v8Util.setDestructor(object, function () {
ipcRenderer.send('FREE', meta.id);
});
return object;
};
在主進程中
const map = {};
const id = 0;
ipcMain.on('REQUIRE', function (event, name) {
const object = require(name);
// Add a reference to the object.
map[++id] = object;
// Convert the object to metadata.
event.returnValue = valueToMeta(id, object);
});
ipcMain.on('FREE', function (event, id) {
delete map[id];
});
具有弱值的映射表
使用先前的簡單實作,remote
模組中的每次呼叫都會從主進程傳回一個新的遠端物件,並且每個遠端物件都代表對主進程中物件的參考。
設計本身很好,但問題在於當有多個呼叫接收同一個物件時,會建立多個代理物件,而對於複雜的物件,這可能會給記憶體使用量和垃圾回收帶來巨大的壓力。
例如,以下程式碼
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}
它首先使用大量記憶體建立代理物件,然後佔用 CPU (中央處理器) 進行垃圾回收並發送 IPC 訊息。
一個顯而易見的優化是快取遠端物件:當已經存在具有相同 ID 的遠端物件時,將傳回先前的遠端物件,而不是建立新的物件。
這對於 JavaScript 核心中的 API 來說是不可能的。使用普通的映射表來快取物件會阻止 V8 垃圾回收物件,而 WeakMap 類別只能將物件用作弱鍵。
為了解決這個問題,新增了一種類型為弱參考值的映射表,這非常適合快取具有 ID 的物件。現在 remote.require
看起來像這樣
const remoteObjectCache = v8Util.createIDWeakMap()
remote.require = function (name) {
// Tell the main process to return the meta data of the module.
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
// Create a proxy object.
...
remoteObjectCache.set(meta.id, object)
return object
}
請注意,remoteObjectCache
將物件儲存為弱參考,因此在物件被垃圾回收時無需刪除鍵。
原生程式碼
對於對 Electron 中弱參考的 C++ 程式碼感興趣的人,可以在以下檔案中找到它
setDestructor
API
createIDWeakMap
API