Administrator
发布于 2026-09-03 / 3,340 阅读
0
0

鸿蒙 NDK 原生开发:CMake 工程、Node-API 与 ArkTS 桥接

鸿蒙百篇系列第 64 篇 · 高级

第 43 篇讲过 NAPI 开发的接口层。这一篇我们把视角拉到"完整 NDK 工程":从新建 Native C++ 工程,到 CMake 编译配置、Node-API 数据类型、跨线程异步回调,以及 ArkTS 侧怎么 import。NDK 不是只为"性能敏感"场景准备的——当你需要复用一段既有 C++ 库、调用系统未公开的能力、或者跑一个算法库时,NDK 都会进视野。

一、什么时候该走 NDK

不是所有 C++ 都需要。决策树大概是这样:

业务诉求

推荐方案

ArkTS 已能满足,纯 UI 交互

不用 NDK

算法、编码、压缩、加解密

NDK

复用现有 C/C++ 库(OpenCV、SQLite 自定义版本)

NDK

极致帧率的自绘游戏

XComponent + EGL(见第 66 篇)

与硬件、驱动、HDI 通信

NDK

ArkTS 调用极短热点

先用 NDK 包装热点,其它代码保持 ArkTS

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,类型由模板决定:

ArkTS 类型

C++ 拿到方式

构造方式

number

napi_get_value_double / _int32

napi_create_double / _int32

string

napi_get_value_string_utf8

napi_create_string_utf8

boolean

napi_get_value_bool

napi_create_boolean

object

napi_get_named_property

napi_create_object

Array

napi_get_element

napi_create_array_with_length

Function (callback)

napi_get_cb_info 后存 napi_ref

入参传入

ArrayBuffer

napi_get_arraybuffer_info

ArkTS 侧传 new ArrayBuffer(x)

这一步是 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 引擎对结构化对象的优化已经很好。

八、工程规范

  1. NDK 模块名跟 bundleName 对齐。nm_modname 写成反域名格式,例如 com.example.app.ndkcrypto,避免 import 冲突

  2. 所有 napi_value 释放用 scope。循环里频繁 napi_create_object 不开 handle scope 是经典的内存炸弹

  3. 跨线程不要传 napi_value。它的 env 是创建时绑定的,换个线程访问即崩溃。要用 napi_ref 拿出来

  4. 错误处理用异常而不是返回码。napi_throw_error(env, nullptr, "message") 是标准做法,ArkTS 侧能 try-catch 到

  5. CMake 里区分 ABI。armeabi-v7a 和 arm64-v8a 分别编译,能省 30-50% 包体积

  6. 静态库 + 头文件方案慎用。如果团队已经有一个大型 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 实战场景。

下一篇进入多窗口开发,看主窗口、子窗口、悬浮窗之间的层级关系是怎么管理的。


评论