簡介
N-API 是 Node.js Addon Programming Interface 的縮寫诈乒,是 Node.js 提供的一組 C++ API,封裝了V8 引擎的能力婆芦,用于編寫 Node.js 的 Native 擴展模塊怕磨。通過 N-API喂饥,開發(fā)者可以使用 C++ 編寫高性能的 Node.js 模塊,同時保持與 Node.js 的兼容性肠鲫。
Node.js 官網(wǎng)中已經(jīng)給出 N-API 接口基礎(chǔ)能力的介紹员帮,同時,方舟 ArkTS 運行時提供的 N-API 接口导饲,封裝了方舟引擎的能力捞高,在功能上與 Node.js 社區(qū)保持一致,這里不再贅述渣锦。
本文將結(jié)合應(yīng)用開發(fā)場景硝岗,分別從對象生命周期管理、跨語言調(diào)用開銷袋毙、異步操作和線程安全四個角度出發(fā)型檀,給出安全、高效的 N-API 開發(fā)指導(dǎo)娄猫。
對象生命周期管理
在進行 N-API 調(diào)用時贱除,引擎堆中對象的句柄 handle 會作為 napi_value 返回,對象的生命周期由這些句柄控制媳溺。對象的句柄會與一個 scope 保持一致月幌,默認情況下,對象當(dāng)前所在 native 方法是 handle 的 scope悬蔽。在應(yīng)用 native 模塊實際開發(fā)過程中扯躺,需要對象有比當(dāng)前所在 native 方法更短或更長的 scope。本文描述了管理對象生命周期的 N-API 接口蝎困,開發(fā)者通過這些接口可以合理的管理對象生命周期录语,滿足業(yè)務(wù)訴求。
縮短對象生命周期
合理使用 napi_open_handle_scope 和 napi_close_handle_scope 管理 napi_value 的生命周期禾乘,做到生命周期最小化澎埠,避免發(fā)生內(nèi)存泄漏問題。
例如始藕,考慮一個具有 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)中會創(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)是一個嵌套的層次結(jié)構(gòu)啊犬,任何時候只有一個存活的 scope,所有新創(chuàng)建的 handle 都將在該 scope 處于存活狀態(tài)時與之關(guān)聯(lián)扩借;
- scope 必須按打開的相反順序關(guān)閉椒惨,在 native 方法中創(chuàng)建的所有 scope 必須在該方法返回之前關(guān)閉。
例如潮罪,使用下面的方法,可以確保在循環(huán)中领斥,最多只有一個句柄是有效的:
// 在for循環(huán)中頻繁調(diào)用napi接口創(chuàng)建js對象時嫉到,要加handle_scope及時釋放不再使用的資源;
// 下面例子中月洛,每次循環(huán)結(jié)束局部變量res的生命周期已結(jié)束何恶,因此加scope及時釋放其持有的js對象,防止內(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;
}
}
存在一些場景细层,某些對象的生命周期需要大于對象本身所在區(qū)域的生命周期,例如嵌套循環(huán)場景唬涧。開發(fā)者可以通過 napi_open_escapable_handle_scope 與 napi_close_escapable_handle_scope 管理對象的生命周期疫赎,在此期間定義的對象的生命周期將與父作用域的生命周期保持一致。
延長對象生命周期
開發(fā)者可以通過創(chuàng)建 napi_ref 來延長 napi_value 對象的生命周期碎节,通過 napi_create_reference 創(chuàng)建的對象需要用戶手動調(diào)用 napi_delete_reference 釋放捧搞,否則可能造成內(nèi)存泄漏。
使用案例1:保存 napi_value
通過 napi_define_class 創(chuàng)建一個 constructor 并保存下來狮荔,后續(xù)可以通過保存的 constructor 調(diào)用 napi_new_instance 來創(chuàng)建實例胎撇。但是,如果 constructor 是以 napi_value 的形式保存下來殖氏,一旦超過了 native 方法的 scope晚树,這個 constructor 就會被析構(gòu),后續(xù)再使用就會造成野指針雅采。推薦寫法如下:
- 1爵憎、開發(fā)者可以改用 napi_ref 的形式把 constructor 保存下來;
- 2、由開發(fā)者自己管理 constructor 對象的生命周期总滩,不受 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 對象的生命周期
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 對象和 js 對象綁定,當(dāng) js 對象被 GC 回收時冈涧,需要通過回調(diào)函數(shù)對 native 對象的資源進行清理茂附。napi_wrap 接口本質(zhì)上也是創(chuàng)建了一個 napi_ref正蛙,開發(fā)者可以根據(jù)業(yè)務(wù)需要,選擇由系統(tǒng)來管理創(chuàng)建的 napi_ref营曼,或是自行釋放創(chuàng)建的 napi_ref乒验。
// 用法1:napi_wrap不需要接收創(chuàng)建的napi_ref,最后一個參數(shù)傳遞nullptr蒂阱,創(chuàng)建的napi_ref由系統(tǒng)管理锻全,不需要用戶手動釋放
napi_wrap(env, jsobject, nativeObject, cb, nullptr, nullptr);
// 用法2:napi_wrap需要接收創(chuàng)建的napi_ref,最后一個參數(shù)不為nullptr录煤,返回的napi_ref需要用戶手動釋放鳄厌,否則會內(nèi)存泄漏
napi_ref result;
napi_wrap(env, jsobject, nativeObject, cb, nullptr, &result);
// 當(dāng)jsobject和result后續(xù)不再使用時,及時調(diào)用napi_remove_wrap釋放result
napi_value result1;
napi_remove_wrap(env, jsobject, result1)
跨語言調(diào)用開銷
接口調(diào)用
跨語言調(diào)用是指在一個程序中使用多種編程語言編寫的代碼妈踊,并且這些代碼可以相互調(diào)用和交互了嚎,ArkTS 調(diào)用 C++ 就是一種跨語言調(diào)用的方式。使用 N-API 進行函數(shù)調(diào)用會引入一定的開銷廊营,因為需要進行上下文切換歪泳、參數(shù)傳遞、函數(shù)調(diào)用和返回值處理等露筒,這些過程都涉及到一些性能開銷呐伞。目前,通過 N-API 接口實現(xiàn) ArkTS 調(diào)用 C++ 的場景大致分為三類:ArkTS 直接調(diào)用 C++ 接口邀窃、ArkTS 監(jiān)聽 C++ 接口以及 ArkTS 接收 C++ 回調(diào)荸哟。頻繁的跨語言接口調(diào)用可能會影響業(yè)務(wù)性能,因此需要開發(fā)者合理的設(shè)計接口調(diào)用頻率瞬捕。
數(shù)值轉(zhuǎn)換
使用 N-API 進行 ArkTS 與 C++ 之間的數(shù)據(jù)轉(zhuǎn)換鞍历,有如下建議:
- 減少數(shù)據(jù)轉(zhuǎn)換次數(shù):頻繁的數(shù)據(jù)轉(zhuǎn)換可能會導(dǎo)致性能下降,可以通過批量處理數(shù)據(jù)或者使用更高效的數(shù)據(jù)結(jié)構(gòu)來優(yōu)化性能肪虎;
- 避免不必要的數(shù)據(jù)復(fù)制:在進行數(shù)據(jù)轉(zhuǎn)換時劣砍,可以使用 N-API 提供的接口來直接訪問原始數(shù)據(jù),而不是創(chuàng)建新的數(shù)據(jù)副本扇救;
- 使用緩存:如果某些數(shù)據(jù)在多次轉(zhuǎn)換中都會被使用到刑枝,可以考慮使用緩存來避免重復(fù)的數(shù)據(jù)轉(zhuǎn)換。緩存可以減少不必要的計算迅腔,提高性能装畅。
異步操作
對于IO、CPU密集型任務(wù)需要異步處理沧烈, 否則會造成主線程的阻塞掠兄。N-API 支持異步能力,允許應(yīng)用程序在執(zhí)行某個耗時任務(wù)時不會被阻塞,而是繼續(xù)執(zhí)行其他任務(wù)蚂夕。當(dāng)異步操作完成時迅诬,應(yīng)用程序會收到通知,并可以處理異步操作的結(jié)果婿牍。
異步示例
開發(fā)者可以通過如下示例將耗時任務(wù)用異步方式實現(xiàn)侈贷,大概邏輯包括以下三步:
- 用 napi_create_promise 接口創(chuàng)建 promise,將創(chuàng)建一個 deferred 對象并與 promise 一起返回等脂,deferred 對象會綁定到已創(chuàng)建的 promise俏蛮;
- 執(zhí)行耗時任務(wù),并將執(zhí)行結(jié)果傳遞給 promise慎菲;
- 使用 napi_resolve_deferred 或 napi_reject_deffered 接口來 resolve 或 reject 創(chuàng)建的 promise嫁蛇,并釋放 deferred 對象。
// 在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í)行耗時任務(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 對象;
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)建一個 deferred 對象并與 promise 一起返回害晦,deferred
// 對象會綁定到已創(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);
// 異步工作項上下文用戶數(shù)據(jù),傳遞到異步工作項的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)建成功后通過最后一個參數(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加到隊列,由底層去調(diào)度執(zhí)行
napi_queue_async_work(env, addonData->asyncWork);
return promise;
}
在異步操作完成后稻轨,回調(diào)函數(shù)將被調(diào)用灵莲,并將結(jié)果傳遞給 Promise 對象。在 JavaScript 中殴俱,可以使用 Promise 對象的 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)先級
Function Flow 編程模型(Function Flow Runtime,F(xiàn)FRT)是一種基于任務(wù)和數(shù)據(jù)驅(qū)動的并發(fā)編程模型线欲,允許開發(fā)者通過任務(wù)及其依賴關(guān)系描述的方式進行應(yīng)用開發(fā)明场。方舟 ArkTS 運行時提供了擴展 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 等級;
qos 等級定義:
typedef enum {
napi_qos_background = 0,
napi_qos_utility = 1,
napi_qos_default = 2,
napi_qos_user_initiated = 3,
} napi_qos_t;
N-API 層封裝了對外的接口魏蔗,對接 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 等級莺治,用于控制任務(wù)調(diào)度的優(yōu)先級廓鞠。使用示例:
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)用需要進行大量的計算或者 IO 操作,使用并發(fā)機制可以充分利用多核 CPU 的優(yōu)勢谣旁,提高應(yīng)用的處理效率床佳。例如,圖像處理榄审、視頻編碼砌们、數(shù)據(jù)分析等應(yīng)用可以使用并發(fā)機制來提高處理速度。
雖然 N-API 本身不支持多線程并發(fā)操作搁进,但是可以在多線程環(huán)境下進行一些數(shù)據(jù)交互浪感,且需要格外注意線程安全眯漩。在多線程環(huán)境下擎值,開發(fā)者可以使用 napi_create_threadsafe_function 函數(shù)創(chuàng)建一個線程安全函數(shù),然后在任意線程中調(diào)用舰始。應(yīng)用場景:當(dāng) native 側(cè)有其他線程莱革,并且需要根據(jù)這些線程的完成結(jié)果調(diào)用 JavaScript 函數(shù)時峻堰,這些線程必須與 native 側(cè)的主線程進行通信,才能在主線程中調(diào)用 JavaScript 函數(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ù)使用注意事項
在多線程環(huán)境下桐筏,需要避免使用共享的數(shù)據(jù)結(jié)構(gòu)和全局變量,以免競爭和沖突拇砰。同時梅忌,需要確保線程之間的同步和互斥,以避免數(shù)據(jù)不一致的情況發(fā)生除破。除此之外牧氮,仍需注意:
- 對線程安全函數(shù)的調(diào)用是異步進行的,對 JavaScript 回調(diào)的調(diào)用將被放置在任務(wù)隊列中瑰枫;
- 創(chuàng)建 napi_threadsafe_function 時踱葛,可以提供 napi_finalize 回調(diào)丹莲。當(dāng)線程安全函數(shù)即將被銷毀時,將在主線程上調(diào)用此 napi_finalize 回調(diào)尸诽;
- 在調(diào)用 napi_create_threadsafe_function 時給定了上下文甥材,可以從任何調(diào)用 napi_get_threadafe_function_context 的線程中獲取。
寫在最后
如果你覺得這篇內(nèi)容對你還蠻有幫助性含,我想邀請你幫我三個小忙:
- 點贊洲赵,轉(zhuǎn)發(fā),有你們的 『點贊和評論』商蕴,才是我創(chuàng)造的動力叠萍。
- 關(guān)注小編,同時可以期待后續(xù)文章ing??绪商,不定期分享原創(chuàng)知識苛谷。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識點,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu