跳转到主要内容

Electron 内部原理:弱引用

·6 分钟阅读

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 会为传入的对象添加一个弱引用,并在对象被垃圾回收时调用回调函数。

// 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