Electron 内部机制:弱引用
作为一种具有垃圾收集功能的语言,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 中测试弱引用。JavaScript 中唯一与弱引用相关的 API 是 WeakMap,但由于它只创建弱引用键,因此无法知道对象何时被垃圾回收。
¥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.
在 Electron v0.37.8 之前的版本中,你可以使用内部 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 无法做到这一点。使用普通映射缓存对象可以防止 V8 引擎对对象进行垃圾回收,而 WeakMap 类只能将对象用作弱键。
¥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.
为了解决这个问题,添加了一个将值作为弱引用的 map 类型,它非常适合缓存带有 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:
createIDWeakMap
API: