在 HarmonyOS 应用开发中,ArkTS 提供了高效的 UI 构建能力,但在处理高性能计算、复用现有的 C/C++ 库或底层系统交互时,我们往往需要深入到 Native 层。NAPI(Node-API)正是连接 ArkTS 与 C++ 的那座桥梁。
本文将结合实战代码,详细介绍 NAPI 的常用函数、C++ 类绑定机制,并重点深入讲解如何在 Native 侧实现异步任务(Async Work),涵盖 Promise 和 Callback 两种常见的调用方式。
一、 NAPI 基础与常用函数
NAPI 是 Node-API 的简写,它提供了一套稳定的 ABI(二进制接口),使得我们可以用 C/C++ 编写高性能模块,并在 ArkTS 中像调用普通函数一样使用它们。
1. 获取调用信息:napi_get_cb_info
当 ArkTS 侧调用一个 Native 方法时,C++ 侧的第一步通常是“解析参数”。napi_get_cb_info 是最基础也是最重要的函数之一。
功能:它用于获取有关函数调用的详细信息,包括:
使用场景:假设你在 Native 侧编写了一个名为 getCbArgs 的函数并导出。当 ArkTS 调用 getCbArgs(arg1, arg2) 时,Native 侧可以通过以下方式获取信息:
// 伪代码示例size_t argc = 2;napi_value args[2];napi_value thisArg;napi_get_cb_info(env, info, &argc, args, &thisArg, nullptr);
注意: 通过 napi_get_cb_info 获取的 this 是 Native 侧的 this。具体来说,它是 Native 侧导出到 ArkTS 侧的对象实例,其中包含了该对象导出的所有成员。这一点在进行混合开发时尤为重要。
2. 调用 JS 函数:napi_call_function
与获取参数相反,当 Native 侧完成计算或需要通知 ArkTS 侧时,我们需要回调 JS 函数。这时就需要使用 napi_call_function。
它允许 Native 代码直接调用 ArkTS 侧传入的函数对象,常用于实现回调(Callback)逻辑。
二、 Node-API 异步任务开发实战
在主线程(UI 线程)中执行耗时的 CPU 密集型任务(如复杂的图像处理、大规模数学运算)会导致应用卡顿甚至 ANR(应用无响应)。因此,利用 NAPI 进行异步任务(Async Work)处理是 Native 开发的重中之重。
1. 异步任务处理流程
ArkTS 调用 NAPI 进行异步任务处理,核心在于将计算逻辑放入工作线程,计算完毕后再将结果抛回主线程。
大致步骤如下:
- 参数解析:读取 ArkTS 传入的参数,并将
napi_value 类型转换为 Native 数据类型(如 double、char*)。 - 上下文封装:定义一个自定义结构体(如
CallbackData),用于在主线程(env)和工作线程之间传递数据(参数、回调函数引用、计算结果等)。 - 创建异步任务:使用
napi_create_async_work 创建任务。我们需要传入两个关键回调:
ExecuteCB:在工作线程执行,处理耗时逻辑,不能调用 NAPI JS 接口。CompleteCB:在主线程执行,当任务完成后触发,用于将结果转换回 ArkTS 数据并调用回调或 Resolve Promise。
- 加入队列:使用
napi_queue_async_work 将任务加入系统调度队列。
2. 实战代码:斐波那契数列计算
下面我们通过一个经典的斐波那契数列(Fibonacci)计算场景,演示如何实现 Promise 和 Callback 两种异步调用方式。
核心结构体与算法
首先,我们需要定义传输数据的结构体和核心算法函数。
#include"napi/native_api.h"// 用于在线程间传递数据的上下文结构体structCallbackData { napi_async_work asyncWork = nullptr; // 异步工作项 napi_deferred deferred = nullptr; // 用于 Promise 的 resolve/reject napi_ref callback = nullptr; // 用于 Callback 的引用double args = 0; // 入参double result = 0; // 计算结果};// 耗时的斐波那契计算(CPU密集型)staticdoublefib(double n){if (n <= 1) {return n; }return fib(n - 1) + fib(n - 2);}
场景一:Execute 回调(通用)
无论是 Promise 还是 Callback,计算逻辑是一样的,都在工作线程中运行。注意:这里纯 C++ 运算,不涉及 NAPI JS 对象操作。
staticvoidExecuteCB(napi_env env, void *data){ CallbackData *callbackData = reinterpret_cast<CallbackData*>(data);// 执行耗时计算 callbackData->result = fib(callbackData->args);}
场景二:Promise 模式实现
Promise 是现代前端开发的主流异步方式。在 Native 侧,我们需要使用 napi_resolve_deferred 或 napi_reject_deferred 来改变 Promise 状态。
完成回调 (CompletePromiseCB):
staticvoidCompletePromiseCB(napi_env env,napi_status status, void *data){ CallbackData *callbackData = reinterpret_cast<CallbackData*>(data); napi_value result = nullptr;// 将 C++ double 结果转回 JS Number napi_create_double(env, callbackData->result, &result);// 根据业务逻辑 Resolve 或 Rejectif (callbackData->result > 0) { napi_resolve_deferred(env, callbackData->deferred, result); } else { napi_reject_deferred(env, callbackData->deferred, result); }// 清理资源 napi_delete_async_work(env, callbackData->asyncWork);delete callbackData; callbackData = nullptr;}
入口函数 (AsyncWork):
static napi_value AsyncWork(napi_env env, napi_callback_info info){size_t argc = 1; napi_value args[1]; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);// 1. 创建 Promise napi_value promise = nullptr; napi_deferred deferred; napi_create_promise(env, &deferred, &promise);// 2. 封装上下文auto callbackData = new CallbackData(); callbackData->deferred = deferred; napi_get_value_double(env, args[0], &callbackData->args);// 3. 创建并入队异步任务 napi_value resourceName = nullptr; napi_create_string_utf8(env, "AsyncCallbackPromise", NAPI_AUTO_LENGTH, &resourceName); napi_create_async_work(env, nullptr, resourceName, ExecuteCB, CompletePromiseCB, callbackData, &callbackData->asyncWork); napi_queue_async_work(env, callbackData->asyncWork);// 4. 返回 Promise 给 ArkTSreturn promise;}
场景三:Callback 模式实现
如果业务方习惯传递回调函数 (result) => {},处理逻辑稍有不同,我们需要持久化存储 callback 引用,并在完成后调用它。
完成回调 (CompleteCallbackCB):
staticvoidCompleteCallbackCB(napi_env env,napi_status status, void *data){ CallbackData *callbackData = reinterpret_cast<CallbackData*>(data); napi_value result = nullptr; napi_create_double(env, callbackData->result, &result);// 获取之前的回调函数引用 napi_value callback; napi_get_reference_value(env, callbackData->callback, &callback);// 构造参数并调用 JS 函数 napi_value tsCallbackArgs[1] = {result}; napi_value callbackResult; napi_call_function(env, nullptr, callback, 1, tsCallbackArgs, &callbackResult);// 清理引用和资源 napi_delete_reference(env, callbackData->callback); napi_delete_async_work(env, callbackData->asyncWork);delete callbackData; callbackData = nullptr;}
入口函数 (AsyncWorkCallback):
static napi_value AsyncWorkCallback(napi_env env, napi_callback_info info){size_t argc = 2; // 期望接收2个参数:数值, 回调函数 napi_value args[2]; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);auto asyncContext = new CallbackData();// 读取数值参数 napi_get_value_double(env, args[0], &asyncContext->args);// 创建回调函数的引用(防止被GC) napi_create_reference(env, args[1], 1, &asyncContext->callback); napi_value resourceName = nullptr; napi_create_string_utf8(env, "AsyncCallback", NAPI_AUTO_LENGTH, &resourceName);// 创建任务,传入 CompleteCallbackCB napi_create_async_work(env, nullptr, resourceName, ExecuteCB, CompleteCallbackCB, asyncContext, &asyncContext->asyncWork); napi_queue_async_work(env, asyncContext->asyncWork);returnnullptr; // 回调模式通常返回 undefined}
3. 模块注册与初始化
最后,别忘了将这两个方法注册到模块中,以便 ArkTS 侧可以导入使用。
EXTERN_C_STARTstatic napi_value Init(napi_env env, napi_value exports){ napi_property_descriptor desc[] = { {"asyncWork", nullptr, AsyncWork, nullptr, nullptr, nullptr, napi_default, nullptr}, {"asyncWorkCallback", nullptr, AsyncWorkCallback, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);return exports;}EXTERN_C_ENDstatic napi_module demoModule = { .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, .nm_modname = "entry1", .nm_priv = ((void *)0), .reserved = {0},};extern"C" __attribute__((constructor)) voidRegisterEntryModule(void){ napi_module_register(&demoModule); }
三、 总结
通过 NAPI,我们可以充分利用 C++ 的生态和性能优势。在开发过程中,异步任务的处理尤为关键:
- 数据隔离:使用自定义结构体
CallbackData 承载数据。 - 线程职责明确:计算逻辑在 Worker 线程,数据转换与 JS 交互在 Main 线程。
- 资源管理:务必记得删除
async_work、napi_ref 以及释放 C++ 内存(delete callbackData),防止内存泄漏。
掌握了这些,你就能在鸿蒙开发中自如地进行 ArkTS 与 Native 的混合编程了。