Skip to main content

Electron 内部原理:弱引用

· 11 min read

作为一种具有垃圾回收功能的语言,JavaScript 免去了用户手动管理资源的麻烦。但由于 Electron 托管了这个环境,它必须非常小心,避免内存和资源泄漏。

🌐 As a language with garbage collection, JavaScript frees users from managing resources manually. But because Electron hosts this environment, it has to be very careful avoiding both memory and resources leaks.

这篇文章介绍了弱引用的概念以及它们如何用于在 Electron 中管理资源。

🌐 This post introduces the concept of weak references and how they are used to manage resources in Electron.


弱引用

🌐 Weak references

在 JavaScript 中,每当你将一个对象赋值给一个变量时,你实际上是添加了对该对象的引用。只要对象有引用存在,它就会一直保留在内存中。一旦对象的所有引用都消失,也就是说不再有变量存储该对象,JavaScript 引擎将在下一次垃圾回收时回收这部分内存。

🌐 In JavaScript, whenever you assign an object to a variable, you are adding a reference to the object. As long as there is a reference to the object, it will always be kept in memory. Once all references to the object are gone, i.e. there are no longer variables storing the object, the JavaScript engine will recoup the memory on next garbage collection.

弱引用是一种对对象的引用,它允许你获取该对象,但不会影响对象是否会被垃圾回收。当对象被垃圾回收时,你也会收到通知。这样就可以使用 JavaScript 来管理资源。

🌐 A weak reference is a reference to an object that allows you to get the object without effecting whether it will be garbage collected or not. You will also get notified when the object is garbage collected. It then becomes possible to manage resources with JavaScript.

以 Electron 中的 NativeImage 类为例,每次调用 nativeImage.create() API 时,都会返回一个 NativeImage 实例,并且它会将图片数据存储在 C++ 中。一旦你使用完成该实例,并且 JavaScript 引擎(V8)已对该对象进行了垃圾回收,C++ 代码会被调用以释放内存中的图片数据,因此用户无需手动管理这一进程。

🌐 Using the NativeImage class in Electron as an example, every time you call the nativeImage.create() API, a NativeImage instance is returned and it is storing the image data in C++. Once you are done with the instance and the JavaScript engine (V8) has garbage collected the object, code in C++ will be called to free the image data in memory, so there is no need for users manage this manually.

另一个例子是 窗口消失问题,它直观地展示了当所有指向窗口的引用消失时,窗口是如何被垃圾回收的。

🌐 Another example is the window disappearing problem, which visually shows how the window is garbage collected when all the references to it are gone.

在 Electron 中测试弱引用

🌐 Testing weak references in Electron

在原生 JavaScript 中没有直接测试弱引用的方法,因为该语言没有分配弱引用的方式。JavaScript 中与弱引用相关的唯一 API 是 弱映射,但由于它只创建弱引用键,因此无法知道对象何时被垃圾回收。

🌐 There is no way to directly test weak references in raw JavaScript since the language doesn't have a way to assign weak references. The only API in JavaScript related to weak references is WeakMap, but since it only creates weak-reference keys, it is impossible to know when an object has been garbage collected.

在 v0.37.8 之前的 Electron 版本中,你可以使用内部 v8Util.setDestructor API 来测试弱引用,该 API 会向传入的对象添加一个弱引用,并在对象被垃圾回收时调用回调函数:

🌐 In versions of Electron prior to v0.37.8, you can use the internal v8Util.setDestructor API to test weak references, which adds a weak reference to the passed object and calls the callback when the object is garbage collected:

// 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 函数。

🌐 Note that you have to start Electron with the --js-flags="--expose_gc" command switch to expose the internal gc function.

该 API 在后续版本中被移除,因为 V8 实际上不允许在析构函数中运行 JavaScript 代码,在后续版本中这样做会导致随机崩溃。

🌐 The API was removed in later versions because V8 actually does not allow running JavaScript code in the destructor and in later versions doing so would cause random crashes.

remote 模块中的弱引用

🌐 Weak references in the remote module

除了使用 C++ 管理本地资源外,Electron 还需要使用弱引用来管理 JavaScript 资源。一个例子是 Electron 的 remote 模块,它是一个 远程进程调用(RPC)模块,允许从渲染进程使用主进程中的对象。

🌐 Apart from managing native resources with C++, Electron also needs weak references to manage JavaScript resources. An example is Electron's remote module, which is a Remote Procedure Call (RPC) module that allows using objects in the main process from renderer processes.

remote 模块的一个关键挑战是避免内存泄漏。当用户在渲染进程中获取远程对象时,remote 模块必须保证该对象在主进程中继续存在,直到渲染进程中的引用消失。此外,它还必须确保当渲染进程中不再有任何引用时,该对象可以被垃圾回收。

🌐 One key challenge with the remote module is to avoid memory leaks. When users acquire a remote object in the renderer process, the remote module must guarantee the object continues to live in the main process until the references in the renderer process are gone. Additionally, it also has to make sure the object can be garbage collected when there are no longer any reference to it in renderer processes.

例如,如果没有适当的实现,下面的代码会很快导致内存泄漏:

🌐 For example, without proper implementation, following code would cause memory leaks quickly:

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}

remote 模块中的资源管理很简单。每当请求一个对象时,会向主进程发送一条消息,Electron 会将该对象存储到一个映射中并为其分配一个 ID,然后将该 ID 发送回渲染进程。在渲染进程中,remote 模块会接收该 ID,并用一个代理对象将其封装起来,当代理对象被垃圾回收时,会向主进程发送消息以释放该对象。

🌐 The resource management in the remote module is simple. Whenever an object is requested, a message is sent to the main process and Electron will store the object in a map and assign an ID for it, then send the ID back to the renderer process. In the renderer process, the remote module will receive the ID and wrap it with a proxy object and when the proxy object is garbage collected, a message will be sent to the main process to free the object.

remote.require API 为例,一个简化的实现如下所示:

🌐 Using remote.require API as an example, a simplified implementation looks like this:

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;
};

在主进程中:

🌐 In the main process:

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];
});

带有弱值的 Map

🌐 Maps with weak values

在之前的简单实现中,remote 模块中的每次调用都会返回来自主进程的新远程对象,每个远程对象都代表对主进程中对象的引用。

🌐 With the previous simple implementation, every call in the remote module will return a new remote object from the main process, and each remote object represents a reference to the object in the main process.

设计本身没问题,但问题在于当多次调用接收同一个对象时,会创建多个代理对象,对于复杂对象,这会对内存使用和垃圾回收带来巨大的压力。

🌐 The design itself is fine, but the problem is when there are multiple calls to receive the same object, multiple proxy objects will be created and for complicated objects this can add huge pressure on memory usage and garbage collection.

例如,以下代码:

🌐 For example, the following code:

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}

它首先使用大量内存来创建代理对象,然后占用 CPU(中央处理器)来回收这些对象并发送 IPC 消息。

🌐 It first uses a lot of memory creating proxy objects and then occupies the CPU (Central Processing Unit) for garbage collecting them and sending IPC messages.

一个明显的优化是缓存远程对象:当已经存在具有相同 ID 的远程对象时,会返回之前的远程对象,而不是创建一个新的对象。

🌐 An obvious optimization is to cache the remote objects: when there is already a remote object with the same ID, the previous remote object will be returned instead of creating a new one.

在 JavaScript 核心中,使用 API 这是不可能的。使用普通的 map 来缓存对象会阻止 V8 对这些对象进行垃圾回收,而 弱映射 类只能将对象作为弱键使用。

🌐 This is not possible with the API in JavaScript core. Using the normal map to cache objects will prevent V8 from garbage collecting the objects, while the WeakMap class can only use objects as weak keys.

为了解决这个问题,添加了一种值为弱引用的映射类型,这对于缓存具有 ID 的对象非常适合。现在 remote.require 看起来是这样的:

🌐 To solve this, a map type with values as weak references is added, which is perfect for caching objects with IDs. Now the remote.require looks like this:

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 将对象存储为弱引用,因此在对象被垃圾回收时无需删除键。

🌐 Note that the remoteObjectCache stores objects as weak references, so there is no need to delete the key when the object is garbage collected.

原生代码

🌐 Native code

对于对 Electron 中弱引用的 C++ 代码感兴趣的人,可以在以下文件中找到:

🌐 For people interested in the C++ code of weak references in Electron, it can be found in following files:

setDestructor API:

🌐 The setDestructor API:

createIDWeakMap API:

🌐 The createIDWeakMap API: