HarmonyOS Next NAPI异步调用入门
为什么需要异步调用?
TS是单线程,如果TS调用C++方法执行耗时任务,比如文件操作、网络请求、数据库操作、图像处理等需要在C++层创建线程来异步执行。如果需要获取异步任务返回结果一般通过回调方式获取。如果要维护原生线程和主线程之间同步需要一些工作量,NAPI基于异步 I/O 库libuv提供了异步调用机制,统一管理线程等,简化开发者使用。
NAPI异步调用介绍
HarmonyOS Next基于OpenHarmony Napi 标准系统异步接口实现支持Callback方式和Promise方式。
异步调用API介绍
- 创建异步work:napi_create_async_work()
napi_status napi_create_async_work(napi_env env,
napi_value async_resource,
napi_value async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete,
void* data,
napi_async_work* result);
参数说明:
- 【in】env: 传入接口调用者的环境,包含js引擎等,由框架提供,默认情况下直接传入即可。
- 【in】async_resource: 可选项,关联async_hooks。
- 【in】async_resource_name: 异步资源标识符,主要用于async_hooks API暴露断言诊断信息。
- 【in】execute: 执行业务逻辑计算函数,由worker线程池调度执行。在该函数中执行IO、CPU密集型任务,不阻塞主线程。
- 【in】complete: execute参数指定的函数执行完成或取消后,触发执行该函数。此函数在EventLoop线程中执行。
- 【in】data: 用户提供的上下文数据,用于传递数据。
- 【out】
result: napi_async_work*
指针,用于返回当前此处函数调用创建的异步工作项。返回值:返回napi_ok表示转换成功,其他值失败。 将异步work加入执行队列:napi_queue_async_work()
napi_status napi_queue_async_work(napi_env env, napi_async_work work);
参数说明:
- 【in】env: 入接口调用者的环境,包含js引擎等,由框架提供,默认情况下直接传入即可。
- 【in】 work:napi_create_async_work创建的句柄
取消异步work:napi_cancel_async_work
napi_status napi_cancel_async_work(napi_env env, napi_async_work work);
参数说明:
- 【in】env: 入接口调用者的环境,包含js引擎等,由框架提供,默认情况下直接传入即可。
- 【in】 work:napi_create_async_work创建的句柄
如果排队的work尚未开始,则取消这个work。如果它已经开始执行,则无法取消,并将返回 napi_generic_failure。如果成功,将使用状态值 napi_cancelled 调用完成回调。即使工作已成功取消,也不应该在完成回调调用之前删除这个work。 删除异步work:napi_delete_async_work
napi_status napi_delete_async_work(napi_env env, napi_async_work work);
参数说明:
- 【in】env: 入接口调用者的环境,包含js引擎等,由框架提供,默认情况下直接传入即可。
- 【in】 work:napi_create_async_work创建的句柄
异步方式实现原理
同步方式中所有的代码处理都在原生方法(主线程)中完成,异步方式依赖NAPI框架提供的napi_create_async_work()函数创建异步工作项,原生方法被调用时,原生方法完成数据接收、转换,存入上下文数据,之后创建一个异步工作项,并加入调度队列,由异步工作线程池统一调度,原生方法返回空值(Callback方式)或返回Promise对象(Promise方式)。
异步工作项中定义了2个函数,一个用于执行工作项的业务逻辑,异步工作项被调度后,该函数从上下文数据中获取输入数据,在worker线程中完成业务逻辑计算(不阻塞主线程)并将结果写入上下文数据。业务逻辑处理函数执行完成或被取消后,触发EventLoop执行另一函数,函数从上下文数据中获取结果,转换为JS类型,调用JS回调函数或通过Promise resolve()返回结果。
异步方式处理流程图
相较原生线程优势
- 线程管理:NodeJs提供了一个抽象层,不需要手动管理线程的生命周期或同步问题。
napi_create_async_work
会在后台执行任务,并在任务完成后自动回调到 JavaScript 线程。Node.js 本身使用了 libuv 作为事件驱动机制,napi_create_async_work
会将任务放入 libuv 的线程池中执行,并在任务完成后通过事件循环调用回调。 - 内存管理:NodeJs 的垃圾回收机制与异步工作结合良好。
napi_create_async_work
确保了在任务完成之前,任务相关的资源不会被提前回收。任务完成后,N-API 会自动管理与 JavaScript 对象的资源同步,提供了便捷的内存和生命周期管理。 - 回调机制:
napi_create_async_work
为 JavaScript 提供了一种与 C++ 异步工作交互的回调机制。任务执行完毕后,回调函数会在主线程中被执行,可以直接将任务结果传回 JavaScript 端。这种机制使得异步工作能够无缝地与 JavaScript 主线程事件循环集成。 - 线程池与并发:napi_create_async_work利用了 Node.js 的 libuv 线程池,无需开发者手动创建和管理线程。Node.js 的线程池会智能调度工作任务,我们不必担心线程池的大小或并发量。
- 错误处理:N-API 提供了较为简单的错误处理方式,任务执行期间的错误可以在回调函数中捕获并传递回 JavaScript 层。开发者可以轻松将本地(Native)代码中的错误状态传递给 JavaScript 层进行处理。
异步接口实现
以音频编码为例展示异步接口。
Callback 异步接口方式
示例接口的eTS定义
export const encodePcmDataForOpusWithOgg:(intputBuffer: ArrayBuffer, callback:() => void) => void;
传入PCM数据,通过编码完成直接写入文件,并同步TS层执行状态(也可以返回编码后数据)。
初始化上下文数据
根据业务需求自定义一个上下文数据结构,用于保存和传递数据。本例自定义的上下文数据包含:异步工作项对象、回调函数、2个参数等4个属性。
struct EncodeAudioData {
napi_async_work asyncWork = nullptr;
napi_ref callback = nullptr;
void* inputBuffer = nullptr;
size_t intputSize = 0;
};
对于NAPI框架,所有参数,无论是ECMAScript标准中定义的Boolean、Null、Undefined、Number、BigInt、String、Symbol和Object八种数据类型,还是Function类型,都已统一封装为napi_value类型,如果可以像获取数据类型的参数一样获取Function类型参数,我们就可以直接调用函数获取2个参数——原始音频数据、callback。
接下来我们将转换后的的参数转换存入上下文数据。
Function类型的参数不能不转换直接存入napi_value类型,因为这涉及到NAPI对象生命周期管理问题。napi_value类型引用对象的生命周期在原生方法退出后结束,后面在work线程无法获取其值。NAPI提供了一种生命期限长于原生方法的对象引用类型napi_ref,napi_ref引用对象在原生方法退出后不自动回收,开发者自己管理此类型对象的生命周期。所以我们需要调用napi_create_reference()函数将接收到的napi_value类型的回调函数参数args[2]
转换为napi_ref类型.
static napi_value encodePCMToOpusOggNative(napi_env env, napi_callback_info info)
{
// 获取2个参数,值的类型是js类型(napi_value)
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 异步工作项上下文用户数据,传递到异步工作项的execute、complete中传递数据
auto audioData = new EncodeAudioData{
.asyncWork = nullptr,
};
// 将接收到的参数传入用户自定义上下文数据
napi_get_arraybuffer_info(env, args[0], &audioData->inputBuffer, &(audioData->intputSize));
napi_create_reference(env, args[1], 1, &audioData->callback);
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "encodePCMToOpusOggNative", NAPI_AUTO_LENGTH, &resourceName);
...
}
创建异步工作项
在创建异步工作项前,我们先分别声明2个函数,分别用作于napi_create_async_work()函数的execute、complete参数。异步工作项创建好,将它存入上下文数据的asyncWork属性,并调用napi_queue_async_work()将异步工作项加入调度队列,由异步work线程池统一调度,原生方法返回空值退出。
// 业务逻辑处理函数,由worker线程池调度执行。
static void encodeExecuteCB(napi_env env, void *data) {
}
// 业务逻辑处理完成回调函数,在业务逻辑处理函数执行完成或取消后触发。
static void encodeAsyncCompleteCB(napi_env env, napi_status status, void *data) {
}
static napi_value encodePCMToOpusOggNative(napi_env env, napi_callback_info info)
{
...
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "encodePCMToOpusOggNative", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName, encodeExecuteCB, encodeAsyncCompleteCB, (void *)audioData, &audioData->asyncWork);
napi_queue_async_work(env, audioData->asyncWork);
return nullptr;
}
execute 函数
execute函数在异步工作项被调度后在work线程中执行,不阻塞主线程(不阻塞UI界面),可执行IO、CPU密集型等任务。此处仅为演示,我们的业务逻辑计算就是一个简单的加法,并把计算结果存入上下文数据的result属性。
// 业务逻辑处理函数,由worker线程池调度执行。
static void aencodeExecuteCB(napi_env env, void *data) {
EncodeAudioData *encodeAudioData = (EncodeAudioData *)data;
//执行音频编码和写入文件
ope_encoder_write(pEnc, (short *)(encodeAudioData->inputBuffer), encodeAudioData->intputSize/2);
}
complete 函数
从接收到的上下文数据中获取结果,调用napi_call_function()方法执行JS回调函数返回数据给JS。之后释放过程中创建的napi_ref引用对象、异步工作项等对象。 NAPI框架提供了napi_call_function()函数供扩展Natvie代码(C/C++代码)调用JS函数,用于执行回调函数等场景。函数定义如下:
NAPI_EXTERN napi_status napi_call_function(napi_env env,
napi_value recv,
napi_value func,
size_t argc,
const napi_value* argv,
napi_value* result);
参数说明:
- 【in】env: 传入接口调用者的环境,包含js引擎等,由框架提供,默认情况下直接传入即可。
- 【in】recv: 传给被调用的this对象。
- 【in】func: 被调用的函数.
- 【in】argc: 函数参数个数(对应函数数组的长度)。
- 【in】 argv: 函数参数数组.
- 【out】 result: func函数执行的返回值。 返回值:返回napi_ok表示转换成功,其他值失败。 因对象生命周期管理问题,上下文数据的callback属性的类型为napi_ref,需要调用napi_get_reference_value()函数获取其指向的napi_value对象值才调用napi_call_function()函数。
complete接口实现:
// 业务逻辑处理完成回调函数,在业务逻辑处理函数执行完成或取消后触发,由EventLoop线程中执行。
static void encodeAsyncCompleteCB(napi_env env, napi_status status, void *data) {
EncodeAudioData *encodeAudioData = (EncodeAudioData *)data;
napi_value callback = nullptr;
napi_value undefined = nullptr;
napi_get_undefined(env, &undefined);
napi_get_reference_value(env, encodeAudioData->callback, &callback);
napi_value callbackResult;
//回调TS
napi_call_function(env, undefined, callback, 0, nullptr, &callbackResult);
//释放资源
if (encodeAudioData->callback != nullptr) {
napi_delete_reference(env, encodeAudioData->callback);
}
napi_delete_async_work(env, encodeAudioData->asyncWork);
delete encodeAudioData;
}
eTS调用接口
static encodePcmDataForOpusOgg(inputBuffer: ArrayBuffer):void {
opusOggEnc.encodePcmDataForOpusWithOgg(inputBuffer, ()=>{
});
}
Promise 接口方式
创建Promise
通过前面异步方式实现原理我们可知Promise整体处理流程和Callback方式一样。不同的是,首先要创建一个Promise。NAPI框架中提供了napi_create_promise()函数用于创建Promise,调用该函数输出2个对象——deferred、promise。promise用于原生方法返回,deferred传入异步工作项的上下文数据。complete函数中,应用napi_resolve_deferred()函数 或 napi_reject_deferred() 函数返回数据。
函数定义如下:
napi_status napi_create_promise(napi_env env,
napi_deferred* deferred,
napi_value* promise);
参数说明:
- 【in】env: 传入接口调用者的环境,包含js引擎等,由框架提供,默认情况下直接传入即可。
- 【in】 deferred: 返回接收刚创建的deferred对象,关联Promise对象,后面使用napi_resolve_deferred() 或 napi_reject_deferred() 返回数据。
- 【out】promise: 关联上面deferred对象的JS Promise对象 返回值:返回napi_ok表示转换成功,其他值失败。
创建Promise接口的实现:
static napi_value encodePromise(napi_env env, napi_callback_info info) {
// 创建promise
napi_value promise = nullptr;
napi_deferred deferred = nullptr;
napi_create_promise(env, &deferred, &promise);
...
// 返回promise
return promise;
}
初始化上下文数据
同Callback方式定义一个上下文数据结构,用于保存和传递数据。Promise方式去掉callback属性,加上deferred属性。
// 用户提供的上下文数据,在原生方法(初始化数据)、executeCB、completeCB之间传递数据
struct EncodeAudioData {
napi_async_work asyncWork = nullptr;
napi_deferred deferred = nullptr;
void* inputBuffer = nullptr;
size_t intputSize = 0;
};
static napi_value encodePromise(napi_env env, napi_callback_info info)
{
// 获取2个参数,值的类型是js类型(napi_value)
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 创建promise
napi_value promise = nullptr;
napi_deferred deferred = nullptr;
napi_create_promise(env, &deferred, &promise);
// 异步工作项上下文用户数据,传递到异步工作项的execute、complete中传递数据
auto audioData = new EncodeAudioData{
.asyncWork = nullptr,
.deferred = deferred,
};
...
}
创建异步工作项
同Callback方式在创建异步工作项前,我们先分别声明2个函数,分别用作于napi_create_async_work()函数的execute、complete参数。异步工作项创建OK后,将其存入上下文数据的asyncWork属性,并调用napi_queue_async_work()将异步工作项加入调度队列,由异步work线程池统一调度,原生方法返回Promise对象退出。
// 用户提供的上下文数据,在原生方法(初始化数据)、executeCB、completeCB之间传递数据
struct EncodeAudioData {
napi_async_work asyncWork = nullptr;
napi_deferred deferred = nullptr;
void* inputBuffer = nullptr;
size_t intputSize = 0;
};
static napi_value encodePromise(napi_env env, napi_callback_info info) {
...
// 创建async work,创建成功后通过最后一个参数(encodeAudioData->asyncWork)返回async work的handle
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "encodeAudioAsyncCallback", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName, encodeExecuteCB, encodePromiseCompleteCB, (void *)audioData,
&audioData->asyncWork);
// 将刚创建的async work加到队列,由底层去调度执行
napi_queue_async_work(env, audioData->asyncWork);
// 原生方法返回promise
return promise;
}
execute 回调处理
此处完全同Callback方式,无需修改。
// 业务逻辑处理函数,由worker线程池调度执行。
static void encodeExecuteCB(napi_env env, void *data) {
EncodeAudioData *encodeAudioData = (EncodeAudioData *)data;
//执行音频编码和写入文件
ope_encoder_write(pEnc, (short *)(encodeAudioData->inputBuffer), encodeAudioData->intputSize/2);
}
complete 回调处理
调用NAPI提供的napi_resolve_deferred() 或 napi_reject_deferred() 返回数据。之后释放过程中创建的napi_ref引用对象、异步工作项等对象。
static void encodePromiseCompleteCB(napi_env env, napi_status status, void *data) {
EncodeAudioData *encodeAudioData = (EncodeAudioData *)data;
napi_value undefined;
napi_get_undefined(env, &undefined);
napi_resolve_deferred(env, encodeAudioData->deferred, undefined);
//删除napi_ref对象
if (encodeAudioData->callback != nullptr) {
napi_delete_reference(env, encodeAudioData->callback);
}
// 删除异步工作项
napi_delete_async_work(env, encodeAudioData->asyncWork);
delete encodeAudioData;
encodeAudioData = nullptr;
}
eTS调用接口
static encodePcmDataForOpusOgg(inputBuffer: ArrayBuffer):void {
opusOggEnc.encodePcmDataForOpusWithOggPromise(inputBuffer).then(()=>{
});
}
规范异步接口
若引擎开启Promise特性支持,则异步方法必须同时支持Callback方式和Promise方式,通过判断接收到的参数个数判断是Callback方式还是Promise方式。
encodePcmDataForOpusWithOggCallback()、encodePcmDataForOpusWithOggPromise() 2个接口合并成一个接口——encodePcmDataForOpusWithOgg(),接口的eTS定义:
export function encodePcmDataForOpusWithOgg(intputBuffer: ArrayBuffer, callback:() => void): void;
export function encodePcmDataForOpusWithOgg(intputBuffer: ArrayBuffer): Promise<void>;
首先修改用户上下文数据结构,同时包含deferred、callback属性。
struct EncodeAudioData {
napi_async_work asyncWork = nullptr;
napi_ref callback = nullptr;
napi_deferred deferred = nullptr;
void* inputBuffer = nullptr;
size_t intputSize = 0;
};
修改接口原生方法实现,通过判断实际获取到的参数个数判断是Callback还是Promise,根据上面的接口定义,2个参数是Promise,3个参数是Callback。
static napi_value encodePCMToOpusOggNative(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
auto audioData = new EncodeAudioData{
.asyncWork = nullptr,
};
if (argc == 1) {
// 创建promise
napi_value promise = nullptr;
napi_deferred deferred = nullptr;
napi_create_promise(env, &deferred, &promise);
audioData->deferred = deferred;
napi_get_arraybuffer_info(env, args[0], &audioData->inputBuffer, &(audioData->intputSize));
napi_create_reference(env, args[1], 1, &audioData->callback);
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "encodePCMToOpusOggNativePromise", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName, encodeExecuteCB, encodePromiseCompleteCB, (void *)audioData,
&audioData->asyncWork);
napi_queue_async_work(env, audioData->asyncWork);
// 返回promise
return promise;
}else{
napi_get_arraybuffer_info(env, args[0], &audioData->inputBuffer, &(audioData->intputSize));
napi_create_reference(env, args[1], 1, &audioData->callback);
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "encodePCMToOpusOggNativeCallback", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName, encodeExecuteCB, encodeAsyncCompleteCB, (void *)audioData, &audioData->asyncWork);
napi_queue_async_work(env, audioData->asyncWork);
return nullptr; }
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。