簡介
N-API 是 Node.js Addon Programming Interface 的縮寫,是 Node.js 提供的一組 C++ API钳枕,封裝了V8 引擎的能力,用于編寫 Node.js 的 Native 擴(kuò)展模塊拐格。通過 N-API吵取,開發(fā)者可以使用 C++ 編寫高性能的 Node.js 模塊,同時(shí)保持與 Node.js 的兼容性聋溜。
Node.js 官網(wǎng)中已經(jīng)給出 N-API 接口基礎(chǔ)能力的介紹谆膳,同時(shí), 方舟 ArkTS 運(yùn)行時(shí) 提供的 N-API 接口撮躁,封裝了方舟引擎的能力漱病,在功能上與 Node.js 社區(qū)保持一致,這里不再贅述把曼。
本文將結(jié)合應(yīng)用開發(fā)場(chǎng)景缨称,分別從對(duì)象生命周期管理、跨語言調(diào)用開銷祝迂、異步操作和線程安全四個(gè)角度出發(fā)睦尽,給出安全、高效的 N-API 開發(fā)指導(dǎo)型雳。
對(duì)象生命周期管理
在進(jìn)行 N-API 調(diào)用時(shí)当凡,引擎堆中對(duì)象的句柄 handle 會(huì)作為 napi_value 返回,對(duì)象的生命周期由這些句柄控制纠俭。對(duì)象的句柄會(huì)與一個(gè) scope 保持一致沿量,默認(rèn)情況下,對(duì)象當(dāng)前所在 native 方法是 handle 的 scope冤荆。在應(yīng)用 native 模塊實(shí)際開發(fā)過程中朴则,需要對(duì)象有比當(dāng)前所在 native 方法更短或更長的 scope。本文描述了管理對(duì)象生命周期的 N-API 接口钓简,開發(fā)者通過這些接口可以合理的管理對(duì)象生命周期乌妒,滿足業(yè)務(wù)訴求汹想。
縮短對(duì)象生命周期
合理使用 napi_open_handle_scope 和 napi_close_handle_scope 管理 napi_value 的生命周期,做到生命周期最小化撤蚊,避免發(fā)生內(nèi)存泄漏問題古掏。
例如,考慮一個(gè)具有 for 循環(huán)的方法侦啸,在該循環(huán)中遍歷獲取大型數(shù)組的元素槽唾,示例代碼如下:
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 循環(huán)中會(huì)創(chuàng)建大量的 handle,消耗大量資源光涂。為了減小內(nèi)存開銷庞萍,N-API 提供創(chuàng)建局部 scope 的能力,在局部 scope 中間所創(chuàng)建 handle 的生命周期將與局部 scpoe 保持一致忘闻。一旦不再需要這些 handle挂绰,就可以直接關(guān)閉局部 scope。
- 打開和關(guān)閉 scope 的方法為 napi_open_handle_scope 和 napi_close_handle_scope服赎;
- N-API 中 scope 的層次結(jié)構(gòu)是一個(gè)嵌套的層次結(jié)構(gòu)葵蒂,任何時(shí)候只有一個(gè)存活的 scope,所有新創(chuàng)建的 handle 都將在該 scope 處于存活狀態(tài)時(shí)與之關(guān)聯(lián)重虑;
- scope 必須按打開的相反順序關(guān)閉践付,在 native 方法中創(chuàng)建的所有 scope 必須在該方法返回之前關(guān)閉。
例如缺厉,使用下面的方法永高,可以確保在循環(huán)中,最多只有一個(gè)句柄是有效的:
// 在for循環(huán)中頻繁調(diào)用napi接口創(chuàng)建js對(duì)象時(shí)提针,要加handle_scope及時(shí)釋放不再使用的資源命爬;
// 下面例子中,每次循環(huán)結(jié)束局部變量res的生命周期已結(jié)束辐脖,因此加scope及時(shí)釋放其持有的js對(duì)象饲宛,防止內(nèi)存泄漏。
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;
}
}
存在一些場(chǎng)景嗜价,某些對(duì)象的生命周期需要大于對(duì)象本身所在區(qū)域的生命周期艇抠,例如嵌套循環(huán)場(chǎng)景。開發(fā)者可以通過 napi_open_escapable_handle_scope 與 napi_close_escapable_handle_scope 管理對(duì)象的生命周期久锥,在此期間定義的對(duì)象的生命周期將與父作用域的生命周期保持一致家淤。
延長對(duì)象生命周期
開發(fā)者可以通過創(chuàng)建 napi_ref 來延長 napi_value 對(duì)象的生命周期,通過 napi_create_reference 創(chuàng)建的對(duì)象需要用戶手動(dòng)調(diào)用 napi_delete_reference 釋放瑟由,否則可能造成內(nèi)存泄漏絮重。
使用案例1:保存 napi_value
通過 napi_define_class 創(chuàng)建一個(gè) constructor 并保存下來,后續(xù)可以通過保存的 constructor 調(diào)用 napi_new_instance 來創(chuàng)建實(shí)例督怜。但是,如果 constructor 是以 napi_value 的形式保存下來,一旦超過了 native 方法的 scope痴施,這個(gè) constructor 就會(huì)被析構(gòu),后續(xù)再使用就會(huì)造成野指針辣吃。推薦寫法如下: * 1、開發(fā)者可以改用 napi_ref 的形式把 constructor 保存下來; * 2厘惦、由開發(fā)者自己管理 constructor 對(duì)象的生命周期,不受 native 方法的 scope 限制宵蕉。
// 1节榜、開發(fā)者可以改用 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羡玛、由開發(fā)者自己管理 constructor 對(duì)象的生命周期
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 napi_get_reference_value(env, g_constructor, &constructor);
};
使用案例2:napi_wrap
開發(fā)者使用 napi_wrap 接口,可以將 native 對(duì)象和 js 對(duì)象綁定宗苍,當(dāng) js 對(duì)象被 GC 回收時(shí)稼稿,需要通過回調(diào)函數(shù)對(duì) native 對(duì)象的資源進(jìn)行清理。napi_wrap 接口本質(zhì)上也是創(chuàng)建了一個(gè) napi_ref讳窟,開發(fā)者可以根據(jù)業(yè)務(wù)需要让歼,選擇由系統(tǒng)來管理創(chuàng)建的 napi_ref,或是自行釋放創(chuàng)建的 napi_ref丽啡。
// 用法1:napi_wrap不需要接收創(chuàng)建的napi_ref谋右,最后一個(gè)參數(shù)傳遞nullptr,創(chuàng)建的napi_ref由系統(tǒng)管理补箍,不需要用戶手動(dòng)釋放
napi_wrap(env, jsobject, nativeObject, cb, nullptr, nullptr);
// 用法2:napi_wrap需要接收創(chuàng)建的napi_ref倚评,最后一個(gè)參數(shù)不為nullptr,返回的napi_ref需要用戶手動(dòng)釋放馏予,否則會(huì)內(nèi)存泄漏
napi_ref result;
napi_wrap(env, jsobject, nativeObject, cb, nullptr, &result);
// 當(dāng)jsobject和result后續(xù)不再使用時(shí)天梧,及時(shí)調(diào)用napi_remove_wrap釋放result
napi_value result1;
napi_remove_wrap(env, jsobject, result1)
跨語言調(diào)用開銷
接口調(diào)用
跨語言調(diào)用是指在一個(gè)程序中使用多種編程語言編寫的代碼,并且這些代碼可以相互調(diào)用和交互霞丧,ArkTS 調(diào)用 C++ 就是一種跨語言調(diào)用的方式呢岗。使用 N-API 進(jìn)行函數(shù)調(diào)用會(huì)引入一定的開銷,因?yàn)樾枰M(jìn)行上下文切換、參數(shù)傳遞后豫、函數(shù)調(diào)用和返回值處理等悉尾,這些過程都涉及到一些性能開銷。目前挫酿,通過 N-API 接口實(shí)現(xiàn) ArkTS 調(diào)用 C++ 的場(chǎng)景大致分為三類:ArkTS 直接調(diào)用 C++ 接口构眯、ArkTS 監(jiān)聽 C++ 接口以及 ArkTS 接收 C++ 回調(diào)。頻繁的跨語言接口調(diào)用可能會(huì)影響業(yè)務(wù)性能早龟,因此需要開發(fā)者合理的設(shè)計(jì)接口調(diào)用頻率惫霸。
數(shù)值轉(zhuǎn)換
使用 N-API 進(jìn)行 ArkTS 與 C++ 之間的數(shù)據(jù)轉(zhuǎn)換,有如下建議: * 減少數(shù)據(jù)轉(zhuǎn)換次數(shù):頻繁的數(shù)據(jù)轉(zhuǎn)換可能會(huì)導(dǎo)致性能下降葱弟,可以通過批量處理數(shù)據(jù)或者使用更高效的數(shù)據(jù)結(jié)構(gòu)來優(yōu)化性能壹店; * 避免不必要的數(shù)據(jù)復(fù)制:在進(jìn)行數(shù)據(jù)轉(zhuǎn)換時(shí),可以使用 N-API 提供的接口來直接訪問原始數(shù)據(jù)芝加,而不是創(chuàng)建新的數(shù)據(jù)副本藏杖; * 使用緩存:如果某些數(shù)據(jù)在多次轉(zhuǎn)換中都會(huì)被使用到抬旺,可以考慮使用緩存來避免重復(fù)的數(shù)據(jù)轉(zhuǎn)換开财。緩存可以減少不必要的計(jì)算责鳍,提高性能历葛。
異步操作
對(duì)于IO恤溶、CPU密集型任務(wù)需要異步處理咒程, 否則會(huì)造成主線程的阻塞帐姻。N-API 支持異步能力饥瓷,允許應(yīng)用程序在執(zhí)行某個(gè)耗時(shí)任務(wù)時(shí)不會(huì)被阻塞呢铆,而是繼續(xù)執(zhí)行其他任務(wù)棺克。當(dāng)異步操作完成時(shí)鼎文,應(yīng)用程序會(huì)收到通知因俐,并可以處理異步操作的結(jié)果。
異步示例
開發(fā)者可以通過如下示例將耗時(shí)任務(wù)用異步方式實(shí)現(xiàn)蓉坎,大概邏輯包括以下三步: * 用 napi_create_promise 接口創(chuàng)建 promise蛉艾,將創(chuàng)建一個(gè) deferred 對(duì)象并與 promise 一起返回勿侯,deferred 對(duì)象會(huì)綁定到已創(chuàng)建的 promise祭埂; * 執(zhí)行耗時(shí)任務(wù)兵钮,并將執(zhí)行結(jié)果傳遞給 promise掘譬; * 使用 napi_resolve_deferred 或 napi_reject_deffered 接口來 resolve 或 reject 創(chuàng)建的 promise葱轩,并釋放 deferred 對(duì)象。
// 在executeCB趾娃、completeCB之間傳遞數(shù)據(jù)
struct AddonData {
napi_async_work asyncWork = nullptr;
napi_deferred deferred = nullptr;
napi_ref callback = nullptr;
double args[2] = {0};
double result = 0;
};
// 2抬闷、執(zhí)行耗時(shí)任務(wù)笤成,并將執(zhí)行結(jié)果傳遞給 promise炕泳;
static void addExecuteCB(napi_env env, void *data) {
AddonData *addonData = (AddonData *)data;
addonData->result = addonData->args[0] + addonData->args[1];
};
// 3培遵、使用 napi_resolve_deferred 或 napi_reject_deffered 接口來 resolve 或 reject 創(chuàng)建的 promise籽腕,并釋放 deferred 對(duì)象;
static void addPromiseCompleteCB(napi_env env, napi_status status, void *data) {
AddonData *addonData = (AddonData *)data;
napi_value result = nullptr;
napi_create_double(env, addonData->result, &result);
napi_resolve_deferred(env, addonData->deferred, result);
if (addonData->callback != nullptr) {
napi_delete_reference(env, addonData->callback);
}
// 刪除異步 work
napi_delete_async_work(env, addonData->asyncWork);
delete addonData;
addonData = nullptr;
};
// 1、用 napi_create_promise 接口創(chuàng)建 promise揍很,將創(chuàng)建一個(gè) deferred 對(duì)象并與 promise 一起返回窒悔,deferred
// 對(duì)象會(huì)綁定到已創(chuàng)建的 promise蛉迹;
static napi_value addPromise(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_value thisArg = nullptr;
napi_get_cb_info(env, info, &argc, args, &thisArg, nullptr);
napi_valuetype valuetype0;
napi_typeof(env, args[0], &valuetype0);
napi_valuetype valuetype1;
napi_typeof(env, args[1], &valuetype1);
if (valuetype0 != napi_number||valuetype1 != napi_number) {
napi_throw_type_error(env, nullptr, "Wrong arguments. 2 numbers expected.");
return NULL;
}
napi_value promise = nullptr;
napi_deferred deferred = nullptr;
napi_create_promise(env, &deferred, &promise);
// 異步工作項(xiàng)上下文用戶數(shù)據(jù)荐操,傳遞到異步工作項(xiàng)的execute托启、complete之間傳遞數(shù)據(jù)
auto addonData = new AddonData{
.asyncWork = nullptr,
.deferred = deferred,
};
napi_get_value_double(env, args[0], &addonData->args[0]);
napi_get_value_double(env, args[1], &addonData->args[1]);
// 創(chuàng)建async work拐迁,創(chuàng)建成功后通過最后一個(gè)參數(shù)(addonData->asyncWork)返回async work的handle
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "addAsyncCallback", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName, addExecuteCB, addPromiseCompleteCB, (void *)addonData,
&addonData->asyncWork);
// 將剛創(chuàng)建的async work加到隊(duì)列线召,由底層去調(diào)度執(zhí)行
napi_queue_async_work(env, addonData->asyncWork);
return promise;
}
在異步操作完成后缓淹,回調(diào)函數(shù)將被調(diào)用讯壶,并將結(jié)果傳遞給 Promise 對(duì)象伏蚊。在 JavaScript 中躏吊,可以使用 Promise 對(duì)象的 then() 方法來處理異步操作的結(jié)果颜阐。
import hilog from '@ohos.hilog';
import testNapi from 'libentry.so'
@Entry
@Component
struct TestAdd {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text("hello world")
.onClick(() => {
let num1 = 2;
let num2 = 3;
testNapi.addPromise(num1, num2).then((result) => {
hilog.info(0x0000, 'testTag', '%{public}d', result);
})
})
}
.width('100%')
.height('100%')
}
}
指定異步任務(wù)調(diào)度優(yōu)先級(jí)
Function Flow 編程模型(Function Flow Runtime瑰艘,F(xiàn)FRT)是一種基于任務(wù)和數(shù)據(jù)驅(qū)動(dòng)的并發(fā)編程模型,允許開發(fā)者通過任務(wù)及其依賴關(guān)系描述的方式進(jìn)行應(yīng)用開發(fā)均蜜。方舟 ArkTS 運(yùn)行時(shí)提供了擴(kuò)展 qos 信息的接口囤耳,支持傳入 qos充择,并調(diào)用 FFRT椎麦,根據(jù)系統(tǒng)資源使用情況降低功耗观挎、提升性能嘁捷。
-
接口示例:napi_status napi_queue_async_work_with_qos(napi_env env, napi_async_work work, napi_qos_t qos)()
- [in] env:調(diào)用API的環(huán)境谜疤;
- [in] napi_async_work: 異步任務(wù)夷磕;
- [in] napi_qos_t: qos 等級(jí)坐桩;
qos 等級(jí)定義:
typedef enum {
napi_qos_background = 0,
napi_qos_utility = 1,
napi_qos_default = 2,
napi_qos_user_initiated = 3,
} napi_qos_t;
N-API 層封裝了對(duì)外的接口,對(duì)接 libuv 層 uv_queue_work_with_qos(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb, uv_qos_t qos) 函數(shù)碾局。
相較于已有接口 napi_queue_async_work净当,增加了 qos 等級(jí)像啼,用于控制任務(wù)調(diào)度的優(yōu)先級(jí)。使用示例: “`cpp static void PromiseOnExec(napi_env env, void *data) { OH_LOG_INFO(LOG_APP, “PromiseOnExec”); }
static void PromiseOnComplete(napi\_env env, napi\_status status, void \*data) {
int number = \*((int \*)data); OH\_LOG\_INFO(LOG\_APP, “PromiseOnComplete number = %{public}d”, number);
}
static napi\_value Test(napi\_env env, napi\_callback\_info info) {
napi\_value resourceName = nullptr;
napi\_create\_string\_utf8(env, “TestExample”, NAPI\_AUTO\_LENGTH, &resourceName);
napi\_async\_work async\_work; int \*data = new int(10); napi\_create\_async\_work(env, nullptr, resourceName, PromiseOnExec, PromiseOnComplete, data, &async\_work);
napi\_queue\_async\_work\_with\_qos(env, async\_work, napi\_qos\_default); return nullptr;
}
線程安全
如果應(yīng)用需要進(jìn)行大量的計(jì)算或者 IO 操作僧诚,使用并發(fā)機(jī)制可以充分利用多核 CPU 的優(yōu)勢(shì),提高應(yīng)用的處理效率衍菱。例如赶么,圖像處理、視頻編碼脊串、數(shù)據(jù)分析等應(yīng)用可以使用并發(fā)機(jī)制來提高處理速度辫呻。
雖然 N-API 本身不支持多線程并發(fā)操作清钥,但是可以在多線程環(huán)境下進(jìn)行一些數(shù)據(jù)交互,且需要格外注意線程安全放闺。在多線程環(huán)境下祟昭,開發(fā)者可以使用 napi_create_threadsafe_function 函數(shù)創(chuàng)建一個(gè)線程安全函數(shù),然后在任意線程中調(diào)用怖侦。
應(yīng)用場(chǎng)景:當(dāng) native 側(cè)有其他線程篡悟,并且需要根據(jù)這些線程的完成結(jié)果調(diào)用 JavaScript 函數(shù)時(shí)搬葬,這些線程必須與 native 側(cè)的主線程進(jìn)行通信抡锈,才能在主線程中調(diào)用 JavaScript 函數(shù)杨幼。線程安全函數(shù)便提供了一種簡化方法歹撒,避免了線程間通訊迈着,同時(shí)可以回到主線程調(diào)用 JavaScript 函數(shù)奴潘。
使用方法
ArkTS 側(cè)傳入回調(diào)函數(shù)
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
testNapi.threadSafeTest((value) => {
hilog.info(0x0000, 'testTag', 'js callback value = ' + value);
})
})
}
.width('100%')
}
.height('100%')
}
}
native 側(cè)主線程中創(chuàng)建線程安全函數(shù)
static void CallJs(napi_env env, napi_value js_cb, void *context, void *data) {
std::thread::id this_id = std::this_thread::get_id();
OH_LOG_INFO(LOG_APP, "thread CallJs %{public}d.\n", this_id);
napi_status status;
status = napi_get_reference_value(env, cbObj, &js_cb);
napi_valuetype valueType = napi_undefined;
napi_typeof(env, js_cb, &valueType);
OH_LOG_INFO(LOG_APP, "CallJs js_cb is napi_function: %{public}d", valueType == napi_function);
OH_LOG_INFO(LOG_APP, "CallJs 0");
if (env != NULL) {
napi_value undefined, js_the_prime;
status = napi_create_int32(env, 666, &js_the_prime);
OH_LOG_INFO(LOG_APP, "CallJs 1: %{public}d", status == napi_ok);
status = napi_get_undefined(env, &undefined);
OH_LOG_INFO(LOG_APP, "CallJs 2: %{public}d", status == napi_ok);
napi_value ret;
status = napi_call_function(env, undefined, js_cb, 1, &js_the_prime, &ret);
OH_LOG_INFO(LOG_APP, "CallJs 3: %{public}d", status == napi_ok);
}
}
napi_threadsafe_function tsfn;
static napi_value ThreadSafeTest(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value js_cb, work_name;
napi_status status;
status = napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL);
OH_LOG_INFO(LOG_APP, "ThreadSafeTest 0: %{public}d", status == napi_ok);
status = napi_create_reference(env, js_cb, 1, &cbObj);
OH_LOG_INFO(LOG_APP, "napi_create_reference of js_cb to cbObj: %{public}d", status == napi_ok);
status =
napi_create_string_utf8(env, "Node-API Thread-safe Call from Async Work Item", NAPI_AUTO_LENGTH, &work_name);
OH_LOG_INFO(LOG_APP, "ThreadSafeTest 1: %{public}d", status == napi_ok);
std::thread::id this_id = std::this_thread::get_id();
OH_LOG_INFO(LOG_APP, "thread ThreadSafeTest %{public}d.\n", this_id);
napi_valuetype valueType = napi_undefined;
napi_typeof(env, js_cb, &valueType);
OH_LOG_INFO(LOG_APP, "ThreadSafeTest js_cb is napi_function: %{public}d", valueType == napi_function);
status = napi_create_threadsafe_function(env, js_cb, NULL, work_name, 0, 1, NULL, NULL, NULL, CallJs, &tsfn);
OH_LOG_INFO(LOG_APP, "ThreadSafeTest 2: %{public}d", status == napi_ok);
}
其他線程中調(diào)用線程安全函數(shù)
std::thread t([]() {
std::thread::id this_id = std::this_thread::get_id();
OH_LOG_INFO(LOG_APP, "thread0 %{public}d.\n", this_id);
napi_status status;
status = napi_acquire_threadsafe_function(tsfn);
OH_LOG_INFO(LOG_APP, "thread1 : %{public}d", status == napi_ok);
status = napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking);
OH_LOG_INFO(LOG_APP, "thread2 : %{public}d", status == napi_ok);
});
t.detach();
線程函數(shù)使用注意事項(xiàng)
在多線程環(huán)境下,需要避免使用共享的數(shù)據(jù)結(jié)構(gòu)和全局變量厚者,以免競爭和沖突。同時(shí),需要確保線程之間的同步和互斥,以避免數(shù)據(jù)不一致的情況發(fā)生互躬。除此之外坎背,仍需注意:
- 對(duì)線程安全函數(shù)的調(diào)用是異步進(jìn)行的湿故,對(duì) JavaScript 回調(diào)的調(diào)用將被放置在任務(wù)隊(duì)列中;
- 創(chuàng)建 napi_threadsafe_function 時(shí),可以提供 napi_finalize 回調(diào)洋魂。當(dāng)線程安全函數(shù)即將被銷毀時(shí)角骤,將在主線程上調(diào)用此 napi_finalize 回調(diào)匙隔;
- 在調(diào)用 napi_create_threadsafe_function 時(shí)給定了上下文,可以從任何調(diào)用 napi_get_threadafe_function_context 的線程中獲取蚊丐。