作为一种具有垃圾回收功能的语言,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 来测试弱引用,该 API 向传递的对象添加弱引用并在对象被垃圾回收时调用回调
var v8Util = process.atomBinding('v8_util');
var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});
object = undefined;
gc();
请注意,您必须使用 --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 会将该对象存储在 map 中并为其分配一个 ID,然后将 ID 发送回渲染器进程。在渲染器进程中,remote
模块将接收 ID 并使用代理对象包装它,并且当代理对象被垃圾回收时,将向主进程发送消息以释放该对象。
以 remote.require
API 为例,简化的实现如下所示
remote.require = function (name) {
const meta = ipcRenderer.sendSync('REQUIRE', name);
const object = metaToValue(meta);
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);
map[++id] = object;
event.returnValue = valueToMeta(id, object);
});
ipcMain.on('FREE', function (event, id) {
delete map[id];
});
具有弱值的 Maps
使用之前的简单实现,remote
模块中的每次调用都会从主进程返回一个新的远程对象,并且每个远程对象都表示对主进程中对象的引用。
设计本身很好,但问题是当多次调用接收同一对象时,将创建多个代理对象,对于复杂的对象,这会给内存使用和垃圾回收带来巨大压力。
例如,以下代码
const { remote } = require('electron');
for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}
它首先使用大量内存创建代理对象,然后占用 CPU(中央处理器)进行垃圾回收并发送 IPC 消息。
一个明显的优化是缓存远程对象:当已经存在具有相同 ID 的远程对象时,将返回之前的远程对象,而不是创建新的远程对象。
这在 JavaScript 核心中的 API 中是不可能的。使用普通 map 缓存对象将阻止 V8 垃圾回收对象,而 WeakMap 类只能使用对象作为弱键。
为了解决这个问题,添加了一种以值作为弱引用的 map 类型,这非常适合缓存具有 ID 的对象。现在 remote.require
看起来像这样
const remoteObjectCache = v8Util.createIDWeakMap()
remote.require = function (name) {
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
...
remoteObjectCache.set(meta.id, object)
return object
}
请注意,remoteObjectCache
将对象存储为弱引用,因此无需在对象被垃圾回收时删除键。
原生代码
对于对 Electron 中弱引用的 C++ 代码感兴趣的人,可以在以下文件中找到它
setDestructor
API
createIDWeakMap
API