鸿蒙百篇系列第 64 篇 · 高级
第 43 篇讲过 NAPI 开发的接口层。这一篇我们把视角拉到"完整 NDK 工程":从新建 Native C++ 工程,到 CMake 编译配置、Node-API 数据类型、跨线程异步回调,以及 ArkTS 侧怎么 import。NDK 不是只为"性能敏感"场景准备的——当你需要复用一段既有 C++ 库、调用系统未公开的能力、或者跑一个算法库时,NDK 都会进视野。
一、什么时候该走 NDK
不是所有 C++ 都需要。决策树大概是这样:
NDK 的代价是"工程复杂度高 + 调试链路长 + 包体积大"。一旦决定用,最好从工程开始就规划好结构。
二、新建一个 Native C++ 工程
DevEco Studio 5.x 之后可以用模板创建 Native C++ 工程,结构如下:
entry/
├── src/main/
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ ├── hello.cpp
│ │ └── types.h
│ ├── ets/
│ │ └── entryability/EntryAbility.ets
│ └── module.json5
├── build-profile.json5
build-profile.json5 里要开启 NDK 编译:
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"buildOption": {
"napiBuildEnabled": true
}
}
]
}
}
CMakeLists.txt 是个简化版:
cmake_minimum_required(VERSION 3.10)
project(NDKDemo)
add_library(entry SHARED hello.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so)
注意 libace_napi.z.so 是 ArkUI 的 C API,libhilog_ndk.z.so 是日志库,NDK 模块基本都会依赖。
三、Node-API 的最小示例
写一个 add(a, b) 给 ArkTS 调。hello.cpp 完整内容:
#include "napi/native_api.h"
#include "hilog/log.h"
static napi_value Add(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_status status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (status != napi_ok) {
napi_throw_error(env, nullptr, "Failed to parse arguments");
return nullptr;
}
double a = 0, b = 0;
napi_get_value_double(env, args[0], &a);
napi_get_value_double(env, args[1], &b);
napi_value result = nullptr;
napi_create_double(env, a + b, &result);
return result;
}
EXTERN_C_START
static napi_module Module = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = [](napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr}
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
},
.nm_modname = "ndkdemo",
.nm_priv = nullptr,
.reserved = {0}
};
extern "C" __attribute__((constructor)) void RegisterModule() {
napi_module_register(&Module);
}
EXTERN_C_END
ArkTS 侧 import:
import ndk from 'libentry.so'; // 和 CMake 里 add_library 的名字一致
const sum = ndk.add(1, 2);
console.info(`1 + 2 = ${sum}`);
libentry.so 的实际位置在编译产物的 entry/libs/<abi>/。工程里加 nativeLibraryPath:
{
"module": {
"nativeLibraryPath": ["libs/arm64-v8a", "libs/x86_64"]
}
}
四、参数传递:从 ArkTS 到 C++
跨语言调用的所有数据都是 napi_value,类型由模板决定:
这一步是 NDK 开发最容易踩坑的地方,每个值的生命周期都要管。一个常见反例:
// ❌ 这段代码在 ArkTS 侧使用值时已经失效
const char* temp;
napi_get_value_string_utf8(env, str, temp, 1024, nullptr);
napi_create_string_utf8(env, temp, ...); // temp 还活着 ✅
// 但是:napi_get_value_string_utf8 第二个参数是 buf
char* buf = new char[1024];
napi_get_value_string_utf8(env, str, buf, 1024, nullptr);
// ... 用完后忘了 delete[] buf
正确做法是栈分配 + scope 管理,或者一次性 napi_get_value_string_utf8 后马上 delete[]。
五、从 C++ 调 ArkTS:异步回调
NDK 模块经常需要"做完耗时任务后回到 UI 线程通知 ArkTS"。Node-API 提供三条主要路径:
方案一:napi_create_promise
适合"一次性返回":
static napi_value SlowAdd(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
double a = 0, b = 0;
napi_get_value_double(env, args[0], &a);
napi_get_value_double(env, args[1], &b);
napi_deferred deferred = nullptr;
napi_value promise = nullptr;
napi_create_promise(env, &deferred, &promise);
auto *state = new AddState{deferred, a + b};
napi_async_work work = nullptr;
napi_create_async_work(env, nullptr, nullptr,
[](napi_env, void *data) {
auto *s = static_cast<AddState*>(data);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
s->result = s->a + s->b;
},
[](napi_env env, napi_status, void *data) {
auto *s = static_cast<AddState*>(data);
napi_value result = nullptr;
napi_create_double(env, s->result, &result);
napi_resolve_deferred(env, s->deferred, result);
napi_delete_async_work(env, s->work);
delete s;
},
state, &work);
state->work = work;
napi_queue_async_work(env, work);
return promise;
}
方案二:napi_create_threadsafe_function
适合"反复回调、流式通知":
struct StreamCtx {
napi_threadsafe_function tsfn;
napi_async_work work;
};
static void CallJs(napi_env env, napi_value jsCb, void *, void *data) {
if (env == nullptr) return;
napi_value msg = nullptr;
napi_create_string_utf8(env, static_cast<const char*>(data), NAPI_AUTO_LENGTH, &msg);
napi_value argv[1] = {msg};
napi_call_function(env, nullptr, jsCb, 1, argv, nullptr);
}
// ArkTS 传进来一个回调
static napi_value StreamOpen(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value jsCb = nullptr;
napi_get_cb_info(env, info, &argc, &jsCb, nullptr, nullptr);
auto *ctx = new StreamCtx();
napi_create_threadsafe_function(
env, jsCb, nullptr,
NAPI_AUTO_LENGTH, 1, nullptr, nullptr,
nullptr, CallJs, &(ctx->tsfn)
);
napi_create_async_work(env, nullptr, nullptr,
// 模拟推流
[](napi_env, void *) {
for (int i = 0; i < 5; i++) {
auto *msg = new std::string("chunk-" + std::to_string(i));
napi_call_threadsafe_function(ctx->tsfn, msg, napi_tsfn_blocking);
}
},
// 收尾
[](napi_env env, napi_status, void *) {
napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);
},
nullptr, &(ctx->work)
);
napi_queue_async_work(env, ctx->work);
return nullptr;
}
方案三:napi_call_threadsafe_function_with_priority
API 12 起的"高优先级"版本,可以让回调插到 ArkTS 消息队列头部:
napi_call_threadsafe_function_with_priority(
ctx->tsfn,
payload,
napi_priority_high, // napi_priority_idle / low / high
true // true=插队头,false=排尾
);
音视频同步、动画插针、关键事件回调都靠它。视频解码完一帧后,用 high 优先级插入 UI 队列头部,比 idle 排在尾部要稳得多。
六、HiLog:从 C++ 打日志
调试 NDK 代码最常用的工具就是 HiLog,头文件 hilog/log.h:
#include "hilog/log.h"
#define LOG_TAG "MyNative"
OH_LOG_INFO(LOG_TAG, "Loaded with param=%{public}d", value);
OH_LOG_ERROR(LOG_TAG, "Failed at step %{public}s, code=%{public}d", name, code);
%{public} 表示参数明文打印,不写的话默认是私有(不会打印到 logcat)。日志过滤:
hdc shell hilog -T MyNative
跟 ArkTS 的 console.info 不是一套,NDK 输出走 hilog 通道。
七、性能与包体积:取舍
要不要 NDK 不能只算开发成本,还要算包体积和启动时间。一个 1MB 的 .so 在鸿蒙上的影响:
APK/APP 包体积增加 1MB
启动时按需加载,不一定拉起
CPU 密集型任务性能比 ArkTS 快 5-10 倍(无 JIT 损耗)
内存峰值略低(无 GC 抖动)
所以"短小高频"的库用 NDK 收益大,"大块统一处理"用 NDK 包体积不值。
做算法迁移前,可以先用 NDK demo 跑一遍真实数据,看加速比。我见过一个团队把 JSON 序列化迁到 NDK,结果反而慢了——ArkTS 的 V8/JIT 比他们手写 C++ 更快,因为现代 JS 引擎对结构化对象的优化已经很好。
八、工程规范
NDK 模块名跟 bundleName 对齐。
nm_modname写成反域名格式,例如com.example.app.ndkcrypto,避免 import 冲突所有 napi_value 释放用 scope。循环里频繁
napi_create_object不开 handle scope 是经典的内存炸弹跨线程不要传 napi_value。它的 env 是创建时绑定的,换个线程访问即崩溃。要用 napi_ref 拿出来
错误处理用异常而不是返回码。
napi_throw_error(env, nullptr, "message")是标准做法,ArkTS 侧能 try-catch 到CMake 里区分 ABI。
armeabi-v7a和arm64-v8a分别编译,能省 30-50% 包体积静态库 + 头文件方案慎用。如果团队已经有一个大型 C++ 库,建议维护成 submodule + 编译脚本,全量编译进 NDK 模块
九、和前几篇的衔接
第 43 篇讲 NAPI 入门,覆盖 Node-API 基本数据类型。本篇补充异步、线程、性能取舍
第 66 篇会用 XComponent + EGL 做自绘,那是 NDK 的另一个重量级用法
第 56 篇讲 IPC,NDK 模块可以作为后台进程通过 IPC 暴露能力
十、小结
NDK 不是 NAPI 的同义词。NAPI 是 Node-API 这一组接口;NDK 是完整的 Native 工程编译、链接、ABI、跨语言调用的全流程。掌握 CMake 工程结构、Node-API 异步三件套和 HiLog 调试方法,已经能覆盖 80% 的 NDK 实战场景。
下一篇进入多窗口开发,看主窗口、子窗口、悬浮窗之间的层级关系是怎么管理的。