简介
N-API 是 Node.js Addon Programming Interface 的缩写,是 Node.js 提供的一组 C++ API,封装了V8 引擎的能力,用于编写 Node.js 的 Native 扩展模块。通过 N-API,开发者可以使用 C++ 编写高性能的 Node.js 模块,同时保持与 Node.js 的兼容性。
Node.js 官网中已经给出 N-API 接口基础能力的介绍,同时,方舟 ArkTS 运行时提供的 N-API 接口,封装了方舟引擎的能力,在功能上与 Node.js 社区保持一致,这里不再赘述。
本文将结合应用开发场景,分别从对象生命周期管理、跨语言调用开销、异步操作和线程安全四个角度出发,给出安全、高效的 N-API 开发指导。
对象生命周期管理
在进行 N-API 调用时,引擎堆中对象的句柄 handle 会作为 napi_value 返回,对象的生命周期由这些句柄控制。对象的句柄会与一个 scope 保持一致,默认情况下,对象当前所在 native 方法是 handle 的 scope。在应用 native 模块实际开发过程中,需要对象有比当前所在 native 方法更短或更长的 scope。本文描述了管理对象生命周期的 N-API 接口,开发者通过这些接口可以合理的管理对象生命周期,满足业务诉求。
缩短对象生命周期
合理使用 napi_open_handle_scope 和 napi_close_handle_scope 管理 napi_value 的生命周期,做到生命周期最小化,避免发生内存泄漏问题。
例如,考虑一个具有 for 循环的方法,在该循环中遍历获取大型数组的元素,示例代码如下:
for (int i = 0; i < 1000000; i++) {
napi_value result;
napi_status status = napi_get_element(env, object, i, &result);
if (status != napi_ok) {
break;
}
// do something with element
}
在 for 循环中会创建大量的 handle,消耗大量资源。为了减小内存开销,N-API 提供创建局部 scope 的能力,在局部 scope 中间所创建 handle 的生命周期将与局部 scpoe 保持一致。一旦不再需要这些 handle,就可以直接关闭局部 scope。
- 打开和关闭 scope 的方法为 napi_open_handle_scope 和 napi_close_handle_scope;
- N-API 中 scope 的层次结构是一个嵌套的层次结构,任何时候只有一个存活的 scope,所有新创建的 handle 都将在该 scope 处于存活状态时与之关联;
- scope 必须按打开的相反顺序关闭,在 native 方法中创建的所有 scope 必须在该方法返回之前关闭。
例如,使用下面的方法,可以确保在循环中,最多只有一个句柄是有效的:
// 在for循环中频繁调用napi接口创建js对象时,要加handle_scope及时释放不再使用的资源;
// 下面例子中,每次循环结束局部变量res的生命周期已结束,因此加scope及时释放其持有的js对象,防止内存泄漏。
for (int i = 0; i < 1000000; i++) {
napi_handle_scope scope;
napi_status status = napi_open_handle_scope(env, &scope);
if (status != napi_ok) {
break;
}
napi_value result;
status = napi_get_element(env, object, i, &result);
if (status != napi_ok) {
break;
}
// do something with element
status = napi_close_handle_scope(env, scope);
if (status != napi_ok) {
break;
}
}
存在一些场景,某些对象的生命周期需要大于对象本身所在区域的生命周期,例如嵌套循环场景。开发者可以通过 napi_open_escapable_handle_scope 与 napi_close_escapable_handle_scope 管理对象的生命周期,在此期间定义的对象的生命周期将与父作用域的生命周期保持一致。
延长对象生命周期
开发者可以通过创建 napi_ref 来延长 napi_value 对象的生命周期,通过 napi_create_reference 创建的对象需要用户手动调用 napi_delete_reference 释放,否则可能造成内存泄漏。
使用案例1:保存 napi_value
通过 napi_define_class 创建一个 constructor 并保存下来,后续可以通过保存的 constructor 调用 napi_new_instance 来创建实例。但是,如果 constructor 是以 napi_value 的形式保存下来,一旦超过了 native 方法的 scope,这个 constructor 就会被析构,后续再使用就会造成野指针。推荐写法如下:
- 1、开发者可以改用 napi_ref 的形式把 constructor 保存下来;
- 2、由开发者自己管理 constructor 对象的生命周期,不受 native 方法的 scope 限制。
// 1、开发者可以改用 napi_ref 的形式把 constructor 保存下来
static napi_value TestDefineClass(napi_env env,
napi_callback_info info) {
napi_status status;
napi_value result, return_value;
napi_property_descriptor property_descriptor = {
"TestDefineClass",
NULL,
TestDefineClass,
NULL,
NULL,
NULL,
napi_enumerable | napi_static,
NULL};
NODE_API_CALL(env, napi_create_object(env, &return_value));
status = napi_define_class(NULL,
"TrackedFunction",
NAPI_AUTO_LENGTH,
TestDefineClass,
NULL,
1,
&property_descriptor,
&result);
SaveConstructor(env, result);
...
}
// 2、由开发者自己管理 constructor 对象的生命周期
napi_status SaveConstructor(napi_env env, napi_value constructor) {
return napi_create_reference(env, constructor, 1, &g_constructor);
};
napi_status GetConstructor(napi_env env) {
napi_value constructor;
return