头图

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()返回结果。

异步方式处理流程图 
image.png

相较原生线程优势

  • 线程管理: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;    }  
}

轻口味
28.1k 声望4.5k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei