本节目标

  • 理解 NDK 在 HarmonyOS 应用开发中的定位与适用场景,掌握 NDK 的组成架构
  • 掌握 Node-API 跨语言交互的核心机制,能够独立完成 C++ 模块的注册、导出与调用
  • 掌握 C++ 侧创建和操作 UI 组件的 NDK 接口,能够使用 ArkUI_NativeNodeAPI_1 构建组件树
  • 掌握 ArkTS 与 C++ 之间复杂数据类型(ArrayBuffer、Object、Class)的传递方法
  • 掌握线程安全函数机制,能够实现 Native 子线程与 UI 主线程的安全通信
  • 掌握 NDK 多线程创建组件能力(API 22+),能够利用多核 CPU 优化页面创建性能
  • 掌握 XComponent + NativeWindow 的自定义渲染管线,能够对接 EGL/OpenGL ES 进行高性能绘制
  • 能够根据场景选择合适的 NDK 方案,并遵循线程安全与内存管理规范

一、NDK 开发概述

1.1 为什么需要 NDK

HarmonyOS 应用开发以 ArkTS 为主,但以下场景需要引入 NDK:

  • 复用已有 C/C++ 库:图形库、音视频处理库、物理引擎、加密库等。
  • 极致性能要求:绕开声明式框架开销,直接操作硬件或进行密集计算。
  • 动态 UI 创建:需要在 Native 侧动态创建和挂载 UI 组件,实现自定义 UI 框架。
  • 底层系统调用:需要使用系统底层能力,而这些能力未通过 ArkTS API 暴露。

NDK 并非替代 ArkTS,而是作为性能补充方案。通用 UI 界面开发仍建议使用 ArkTS 声明式框架。

1.2 NDK 的组成架构

NDK 主要由以下部分组成:

  • Native Module:开发者使用 Node-API 开发的模块,供 ArkTS 侧导入使用。
  • Node-API:实现 ArkTS 与 C/C++ 交互的逻辑层。
  • ModuleManager:Native 模块管理,包括加载、查找等。
  • ScopeManager:管理 napi_value 的生命周期。
  • ReferenceManager:管理 napi_ref 的生命周期。
  • NativeEngine:ArkTS 引擎抽象层,统一 ArkTS 引擎在 Node-API 层的接口行为。

1.3 适用场景与选型原则

  • 优先使用系统 Kit:HarmonyOS 已提供丰富的系统能力,优先使用 ArkTS API。
  • 仅在必要时引入 NDK:性能敏感、需要复用 C/C++ 库、需要动态创建 UI 时才使用。
  • 注意包体积影响:Native 库会增加包体积,需配合 strip 和 compressNativeLibs 优化。
  • 线程安全第一:Native 子线程不能直接调用 ArkTS 函数,必须使用线程安全函数。

二、Node-API 跨语言交互机制

2.1 Node-API 核心原理

HarmonyOS Node-API 基于 Node.js 18.x LTS 的 Node-API 规范扩展开发,提供一组稳定的 C/C++ API,实现 ArkTS/JS 与 C/C++ 模块之间的交互。其核心机制是:C/C++ 模块注册时向 ArkTS 对象挂载属性和方法,ArkTS 调用时引擎找到并执行对应的 C/C++ 函数。

交互分为两个阶段:

  • 初始化阶段:ArkTS 侧 import Native 模块时,引擎加载模块对应的 so,触发模块注册,将方法属性挂载到 exports 对象并返回。
  • 调用阶段:ArkTS 侧调用 import 返回的对象上的方法时,引擎找到并调用对应的 C/C++ 方法。

2.2 Native C++ 工程创建

在 DevEco Studio 中创建 NDK 工程:

  1. 选择 New → Create Project。
  2. 选择 Native C++ 模板。
  3. 配置 API 版本和工程名称,点击 Finish。

创建成功后,工程目录下会包含:

  • cpp 目录:存放 C++ 源码和 CMakeLists.txt。
  • types 目录:存放 Native 模块的 TypeScript 声明文件。

2.3 C++ 模块注册与导出完整示例

以下是一个完整的 Native 模块实现,包含加法函数和字符串拼接函数:

// napi_init.cpp
#include "napi/native_api.h"
#include <string>

// 加法函数
static napi_value Add(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);

    double a, b;
    napi_get_value_double(env, args[0], &a);
    napi_get_value_double(env, args[1], &b);

    napi_value result;
    napi_create_double(env, a + b, &result);
    return result;
}

// 字符串拼接函数
static napi_value Concat(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);

    // 获取字符串长度
    size_t len1, len2;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &len1);
    napi_get_value_string_utf8(env, args[1], nullptr, 0, &len2);

    // 分配内存并读取字符串
    char* str1 = new char[len1 + 1];
    char* str2 = new char[len2 + 1];
    napi_get_value_string_utf8(env, args[0], str1, len1 + 1, &len1);
    napi_get_value_string_utf8(env, args[1], str2, len2 + 1, &len2);

    std::string result = std::string(str1) + std::string(str2);

    // 创建返回值
    napi_value resultValue;
    napi_create_string_utf8(env, result.c_str(), result.length(), &resultValue);

    delete[] str1;
    delete[] str2;
    return resultValue;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"concat", nullptr, Concat, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = nullptr,
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
    napi_module_register(&demoModule);
}

在 ArkTS 侧引用:

import nativeModule from 'libentry.so';

const sum = nativeModule.add(3, 4);          // 7
const text = nativeModule.concat('Hello ', 'NDK'); // 'Hello NDK'

2.4 模块加载与调用流程

  1. ArkTS 执行 import nativeModule from 'libentry.so'。
  2. 引擎调用 ModuleManager 加载 libentry.so。
  3. 触发 RegisterEntryModule,执行 napi_module_register。
  4. 引擎调用 Init 函数,将 add、concat 等方法挂载到 exports 对象。
  5. ArkTS 拿到导出对象,调用方法时引擎定位到对应的 C++ 函数并执行。

三、C++ 侧创建与操作 UI 组件

3.1 ArkUI_NativeNodeAPI_1 接口集

ArkUI 提供 NDK 接口集 ArkUI_NativeNodeAPI_1,允许在 C/C++ 侧创建 UI 组件、操作组件树、设置属性和监听事件。核心方法:

  • createNode:创建组件对象,返回 ArkUI_NodeHandle。
  • disposeNode:销毁组件对象。
  • addChild:将组件挂载到父节点。
  • removeChild:从父节点移除组件。
  • insertChildAfter:在指定子节点后插入新节点。
  • setAttribute:设置组件属性。
  • registerNodeEvent:注册事件回调。

3.2 创建组件树完整示例

以下示例在 C++ 侧创建一个 Column 容器,包含 Text 和 Button:

#include <arkui/native_node.h>
#include <arkui/native_interface.h>

ArkUI_NativeNodeAPI_1* nodeAPI = nullptr;

ArkUI_NodeHandle CreateUITree() {
    // 获取 NDK 接口
    OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, nodeAPI);

    // 创建 Column 容器
    ArkUI_NodeHandle column = nodeAPI->createNode(ARKUI_NODE_COLUMN);
    ArkUI_NumberValue widthValue[] = {300};
    ArkUI_AttributeItem widthItem = {widthValue, 1};
    nodeAPI->setAttribute(column, NODE_WIDTH, &widthItem);
    nodeAPI->setAttribute(column, NODE_HEIGHT, &widthItem);

    // 创建 Text 组件
    ArkUI_NodeHandle text = nodeAPI->createNode(ARKUI_NODE_TEXT);
    ArkUI_AttributeItem textContent = {
        .string = "Hello from C++"
    };
    nodeAPI->setAttribute(text, NODE_TEXT_CONTENT, &textContent);
    ArkUI_NumberValue fontSize[] = {24};
    ArkUI_AttributeItem fontSizeItem = {fontSize, 1};
    nodeAPI->setAttribute(text, NODE_FONT_SIZE, &fontSizeItem);

    // 创建 Button 组件
    ArkUI_NodeHandle button = nodeAPI->createNode(ARKUI_NODE_BUTTON);
    ArkUI_AttributeItem buttonLabel = {
        .string = "C++ Button"
    };
    nodeAPI->setAttribute(button, NODE_BUTTON_LABEL, &buttonLabel);

    // 组装组件树
    nodeAPI->addChild(column, text);
    nodeAPI->addChild(column, button);

    return column;
}

3.3 挂载到 ArkTS 页面

NDK 创建的 UI 组件需要通过 ArkTS 层的 NodeContainer 挂载显示:

import { NodeController, FrameNode, UIContext } from '@kit.ArkUI';

class NativeNodeController extends NodeController {
  private rootNode: FrameNode | null = null;

  makeNode(uiContext: UIContext): FrameNode | null {
    // 调用 Native 接口创建 UI 树,返回 FrameNode
    this.rootNode = nativeModule.createNativeUITree(uiContext);
    return this.rootNode;
  }
}

@Entry
@Component
struct NativeUIDemo {
  private controller: NativeNodeController = new NativeNodeController();

  build() {
    Column() {
      NodeContainer(this.controller)
        .width('100%')
        .height(300)
    }
  }
}

3.4 事件绑定与属性设置

为 Button 注册点击事件:

static void OnButtonClick(ArkUI_NodeEvent* event) {
    // 处理点击事件
    OH_LOG_Print(LOG_APP, LOG_INFO, 0, "NDK", "Button clicked");
}

// 在创建 Button 后注册事件
nodeAPI->registerNodeEvent(button, NODE_ON_CLICK, 0, nullptr);
nodeAPI->addNodeEventReceiver(button, OnButtonClick);

四、ArkTS 与 C++ 高性能数据交互

4.1 基本数据类型传递

  • number:napi_get_value_double / napi_create_double
  • string:napi_get_value_string_utf8 / napi_create_string_utf8
  • boolean:napi_get_value_bool / napi_create_boolean

4.2 复杂类型传递

ArrayBuffer / Uint8Array:

// 获取 ArrayBuffer 数据
void* data;
size_t length;
napi_get_arraybuffer_info(env, args[0], &data, &length);

// 创建 ArrayBuffer 返回
napi_value result;
void* newData;
napi_create_arraybuffer(env, length, &newData, &result);
memcpy(newData, data, length);

Object 类型:使用 napi_create_object_with_named_properties 构建对象:

napi_value CreateObject(napi_env env) {
    napi_value result;
    const char* keys[] = {"name", "age"};
    napi_value values[2];
    napi_create_string_utf8(env, "Alice", NAPI_AUTO_LENGTH, &values[0]);
    napi_create_int32(env, 25, &values[1]);
    napi_create_object_with_named_properties(env, &result, 2, keys, values);
    return result;
}

Class 类型:通过 napi_define_class 包装 C++ 类,实现构造函数和方法导出。

4.3 线程安全函数与多线程通信

ArkTS 函数只能在主线程调用。Native 子线程不能直接使用主线程的 napi_env 和 napi_value,必须通过线程安全函数(Threadsafe Function)投递回主线程。

最佳实践:

  • 在 Native 初始化时(主线程)创建一次全局唯一的 threadsafe function。
  • 子线程通过 napi_call_threadsafe_function 投递结果。
  • 每个任务独立分配 data,回调中释放。
  • 生产线程动态增减时,使用 napi_acquire_threadsafe_function / napi_release_threadsafe_function。
// 主线程创建
napi_create_threadsafe_function(env, jsCallback, nullptr, "tsfn",
    256, 1, context, Finalize, context, CallJs, &tsfn);

// 子线程投递
auto* msg = new Message(payload);
napi_call_threadsafe_function(tsfn, msg, napi_tsfn_nonblocking);

// 回调中处理并释放
void CallJs(napi_env env, napi_value jsCallback, void* context, void* data) {
    auto* msg = static_cast<Message*>(data);
    // 调用 ArkTS 回调
    napi_call_function(env, global, jsCallback, 0, nullptr, nullptr);
    delete msg;
}

4.4 NDK 多线程创建组件(API 22+)

从 API 22 开始,NDK 支持在任意线程创建 UI 组件并设置属性,但挂载到 UI 主树必须在 UI 线程执行。

获取多线程接口:

ArkUI_NativeNodeAPI_1 *multiThreadNodeAPI = nullptr;
OH_ArkUI_GetModuleInterface(ARKUI_MULTI_THREAD_NATIVE_NODE,
                             ArkUI_NativeNodeAPI_1, multiThreadNodeAPI);
auto node = multiThreadNodeAPI->createNode(ARKUI_NODE_COLUMN);

优势:充分利用多核 CPU,降低页面创建耗时,UI 线程专注于动画渲染与输入响应。

五、XComponent + NativeWindow 自定义渲染

5.1 XComponent 两种模式

  • SURFACE 类型:独立 Surface,不和组件树合成,直接上屏,系统合成层自动 Y 翻转修正。适用于游戏渲染、相机预览等独立高性能渲染场景。
  • TEXTURE 类型:走 UI 纹理贴图,与组件树合成后展示,天然支持层级混合。适用于需要与 ArkUI 混合渲染的场景,但需手动处理 Y 翻转。

5.2 NativeWindow 渲染管线

完整流程:

  1. ArkUI 创建 XComponent。
  2. 获取 Surface ID,通过 NAPI 传递给 C++。
  3. C++ 获取 ANativeWindow。
  4. 初始化 EGL/GLES。
  5. 进入渲染循环,每帧绘制。
  6. ArkUI 控制生命周期(启动/停止/销毁)。

5.3 EGL/OpenGL ES 集成要点

CMake 链接:

target_link_libraries(entry PUBLIC
    libace_napi.z.so
    libEGL.so
    libGLESv3.so
    libnative_window.so
)

5.4 Buffer 同步与异常处理

申请 Buffer 后必须等待 release fence 信号才能写入:

OHNativeWindowBuffer* buffer = nullptr;
int32_t releaseFence = -1;
OH_NativeWindow_RequestBuffer(nativeWindow, &buffer, &releaseFence);

if (releaseFence != -1) {
    poll(&(struct pollfd){.fd = releaseFence, .events = POLLIN}, 1, -1);
    close(releaseFence);
}

void* virAddr = nullptr;
OH_NativeWindow_MapBuffer(buffer, &virAddr);
memcpy(virAddr, pixelData, dataSize);

int32_t acquireFence = -1;
OH_NativeWindow_FlushBuffer(nativeWindow, buffer, -1, acquireFence);

异常处理:申请到 Buffer 后必须 FlushBuffer 或 AbortBuffer,否则队列耗尽导致后续 RequestBuffer 阻塞。

六、性能优化与最佳实践

6.1 减少跨语言调用开销

  • 批量传递数据,减少调用次数。
  • 使用 ArrayBuffer 零拷贝(HarmonyOS 7.0+)避免大数据拷贝。
  • 耗时操作放在 Native 子线程,避免阻塞 UI 线程。

6.2 内存管理与资源释放

  • 每个 napi_create_* 创建的对象需正确管理生命周期。
  • 使用 napi_create_reference 持久化引用时,需在适当时机 napi_delete_reference。
  • Native 侧动态分配的内存需及时释放,避免内存泄漏。

6.3 常见问题排查

  • 崩溃在 napi_call_function:检查是否在子线程直接调用了 ArkTS 函数。
  • 组件不显示:检查是否将 NDK 创建的节点正确挂载到 NodeContainer。
  • 渲染花屏:检查 Buffer 同步逻辑,确保等待了 release fence。
  • 包体积过大:配置 strip 和 compressNativeLibs,精简 ABI。

七、多元化习题

习题 1(判断题)

题目:在 HarmonyOS 中,NDK 主要用于替代 ArkTS,实现所有 UI 界面的开发。

答案:错误

解读:NDK 并非替代 ArkTS,而是作为性能补充方案。面向通用 UI 界面开发场景,建议使用 ArkTS 和 ArkUI 声明式框架。NDK 适用于复用 C/C++ 库、高性能计算、动态 UI 组件创建等场景。

习题 2(单选题)

题目:以下哪个 Node-API 接口用于在 Native 侧创建带有给定属性值的 object 类型对象?

A. napi_get_arraybuffer_info
B. napi_create_object_with_named_properties
C. napi_call_threadsafe_function
D. napi_create_arraybuffer

答案:B

解读:napi_create_object_with_named_properties 用于从 C++ 侧传递 object 类型到 ArkTS 侧时构建带有给定属性值的 object 对象。其他选项分别用于获取 ArrayBuffer 信息、调用线程安全函数、创建 ArrayBuffer。

习题 3(多选题)

题目:关于 NDK 多线程创建组件,以下说法正确的有(多选):

A. API 22 之前,UI 组件创建必须在 UI 线程中执行
B. API 22 支持在任意线程中直接调用组件创建接口
C. 组件创建和属性设置支持多线程并发调用,可充分利用多核 CPU
D. 可以在任意线程中把 UI 组件挂载到 UI 主树上

答案:A、B、C

解读:API 22 之前,UI 组件创建与属性设置必须在 UI 线程执行,选项 A 正确。API 22 引入多线程支持,可在任意线程创建组件和设置属性,选项 B 正确。多线程并发调用可充分利用多核 CPU,选项 C 正确。可以在任意线程创建组件,但必须在 UI 线程中挂载到 UI 主树,选项 D 错误。

习题 4(代码填空题)

题目:请补全以下代码,使用 Node-API 创建一个导出的 Native 方法。

static napi_value Add(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);

    double a, b;
    napi_get_value_double(env, args[0], &a);
    napi_get_value_double(env, args[1], &b);

    napi_value result;
    // 在此处填写代码,创建 double 类型返回值
    ______________
    return result;
}

static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    // 在此处填写代码,将方法挂载到 exports 对象
    ______________
    return exports;
}

答案:napi_create_double(env, a + b, &result);、napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);

解读:napi_create_double 创建 double 类型返回值。napi_define_properties 将方法属性数组挂载到 exports 对象,使 ArkTS 侧可以通过 import 使用。

习题 5(代码改错题)

题目:以下代码存在线程安全问题,请指出问题并修正。

void NativeWorkerThread() {
    std::thread worker([&]() {
        napi_value result;
        napi_call_function(env, globalCallback, nullptr, 0, nullptr, &result);
    });
    worker.detach();
}

答案:ArkTS 函数只能在主线程调用,Native 子线程不能直接使用主线程的 napi_env 和 napi_value 调用 ArkTS 函数。必须使用线程安全函数将结果传递回主线程后再调用。修正方案:

napi_threadsafe_function tsfn;
napi_create_threadsafe_function(env, arkTSCallback, nullptr, "tsfn",
    0, 1, nullptr, nullptr, nullptr, CallJs, &tsfn);

std::thread worker([&]() {
    napi_call_threadsafe_function(tsfn, resultData, napi_tsfn_nonblocking);
});
worker.detach();

解读:napi_env 与线程绑定,子线程中不能直接使用主线程的 napi_env。必须通过线程安全函数将结果投递回主线程,由主线程事件循环调度执行 ArkTS 回调。

习题 6(简答题)

题目:简述 Node-API 在 HarmonyOS 中的核心作用,以及 ArkTS 与 C++ 交互的两个主要阶段。

答案:Node-API 是 HarmonyOS 提供的 ArkTS/JS 与 C/C++ 模块之间的交互能力,封装了 I/O、CPU 密集型、OS 底层等能力并对外暴露 C 接口,通过模块注册机制向 ArkTS/JS 对象上挂载属性和方法。交互的两个主要阶段:初始化阶段,当 ArkTS 侧 import 一个 Native 模块时,ArkTS 引擎调用 ModuleManager 加载模块对应的 so 及其依赖,首次加载时触发模块注册,将模块定义的方法属性挂载到 exports 对象上并返回;调用阶段,当 ArkTS 侧通过 import 返回的对象调用方法时,ArkTS 引擎找到并调用对应的 C/C++ 方法。

习题 7(简答题)

题目:简述使用线程安全函数实现 Native 子线程与 UI 主线程通信的完整流程。

答案:完整流程包括:第一步,ArkTS 侧传递一个回调函数到 Native 侧。第二步,Native 侧在主线程中创建线程安全函数,绑定 ArkTS 回调函数,保存上下文信息及参数,然后拆分子线程。第三步,Native 侧子线程分配到系统资源后执行对应业务逻辑,通过 napi_call_threadsafe_function 接口调用线程安全函数。第四步,线程安全函数被 push 到主线程的事件循环中等待调度执行。第五步,线程安全函数在事件循环中得到调度后,通过回调函数调用 ArkTS 侧传递的回调函数。最佳实践是复用一个全局唯一的线程安全函数,不要每个数据包都创建。

解读:线程安全函数机制的核心在于将子线程的计算结果安全地传递到主线程执行 ArkTS 回调。直接跨线程使用 napi_env 会导致未定义行为,必须通过线程安全函数进行同步。

八、本节知识点总结

NDK 定位
NDK 是 HarmonyOS SDK 提供的 Native API、编译脚本和工具链集合,用于 C/C++ 实现关键功能。适用于复用 C/C++ 库、高性能计算、动态 UI 组件创建等场景,是 ArkTS 的性能补充方案而非替代。

Node-API 交互机制
基于 Node.js 18.x LTS 规范扩展,通过 C/C++ 模块注册机制向 ArkTS/JS 对象挂载方法和属性。交互分为初始化阶段(模块加载与注册)和调用阶段(方法调用与执行)。

C++ 侧创建 UI 组件
使用 ArkUI_NativeNodeAPI_1 接口集,通过 createNode 创建组件、addChild 组装组件树、setAttribute 设置属性。NDK 创建的组件需通过 ArkTS 的 NodeContainer 挂载到 UI 树上显示。

复杂数据传递
ArrayBuffer/Uint8Array 通过 napi_get_arraybuffer_info 获取指针;Object 通过 napi_create_object_with_named_properties 构建;Class 通过 Node-API 包装机制导出。

线程安全函数
ArkTS 函数只能在主线程调用,Native 子线程需通过线程安全函数将结果传递回主线程。创建一次全局线程安全函数,子线程通过 napi_call_threadsafe_function 投递结果。

NDK 多线程创建组件
API 22 起支持在任意线程创建 UI 组件和设置属性,需通过 ARKUI_MULTI_THREAD_NATIVE_NODE 获取多线程接口集合。组件挂载必须在 UI 线程执行。

XComponent 自定义渲染
通过 SURFACE 模式暴露 NativeWindow,使用 EGL/OpenGL ES 写入渲染数据,适用于游戏渲染和相机预览等高性能场景。Buffer 同步需等待 release fence,异常路径需调用 AbortBuffer 归还 Buffer。

下节预告

第5课将进入 ArkUI 的编译优化与包体积进阶的学习,涵盖 ArkTS 编译优化、字节码裁剪、资源压缩、so 库优化、HSP 共享以及包体积分析工具链的深度使用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐