Electron 內部原理:弱引用
作為一種具有垃圾收集功能的語言,JavaScript 解放了使用者手動管理資源的負擔。但是,由於 Electron 託管此環境,因此它必須非常小心地避免記憶體和資源洩漏。
這篇文章介紹了弱引用的概念,以及它們在 Electron 中如何用於管理資源。
弱引用
在 JavaScript 中,每當您將物件指派給變數時,您都會新增對該物件的參考。只要有對該物件的參考,它就會一直保留在記憶體中。一旦對物件的所有參考都消失了,也就是說,不再有任何變數儲存該物件,JavaScript 引擎將在下次垃圾收集時回收記憶體。
弱引用是對物件的參考,它允許您取得物件,而不會影響它是否會被垃圾收集。當物件被垃圾收集時,您也會收到通知。這樣就可以使用 JavaScript 管理資源。
以 Electron 中的 NativeImage
類別為例,每次您呼叫 nativeImage.create()
API 時,都會傳回 NativeImage
執行個體,並且它會在 C++ 中儲存影像資料。一旦您用完該執行個體,且 JavaScript 引擎 (V8) 已垃圾收集該物件,就會呼叫 C++ 中的程式碼來釋放記憶體中的影像資料,因此使用者無需手動管理。
另一個範例是視窗消失問題,這在視覺上顯示了當所有對它的參考都消失時,視窗是如何被垃圾收集的。
在 Electron 中測試弱引用
在原始 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 並用 Proxy 物件包裝它,當 Proxy 物件被垃圾收集時,就會將訊息傳送至主程序以釋放該物件。
以 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
模組中的每次呼叫都會從主程序傳回新的遠端物件,而每個遠端物件都代表對主程序中物件的參考。
該設計本身沒有問題,但問題在於當多次呼叫接收同一個物件時,會建立多個 Proxy 物件,而對於複雜的物件,這可能會對記憶體使用和垃圾收集增加巨大的壓力。
例如,以下程式碼
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}
它首先使用大量記憶體建立 Proxy 物件,然後佔用 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