在鸿蒙(HarmonyOS)中进行 C++ 混合开发,调用 OpenCV 或 FFmpeg 等底层 C/C++ 库,其核心技术是 NAPI (Node-API)。整体架构分为 ArkTS 前端调用、NAPI 接口桥接、C++ 业务逻辑处理以及底层三方库执行四个层级。
以下是完整的实战开发流程与代码示例:
一、 工程配置与依赖引入
在 NDK 工程中使用预构建的 C++ 库(如 FFmpeg 或 OpenCV),需要通过 CMake 进行配置。
1. 引入预构建库(以 FFmpeg 为例)
将预构建的 .so 文件放入工程的 libs 目录,并在 CMakeLists.txt 中声明并链接:
javascript
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(MyNativeApp)
# 添加预构建的 FFmpeg 库
add_library(avcodec_ffmpeg SHARED IMPORTED)
set_target_properties(avcodec_ffmpeg PROPERTIES IMPORTED_LOCATION
${CMAKE_CURRENT_SOURCE_DIR}/third_party/FFmpeg/libs/${OHOS_ARCH}/libavcodec_ffmpeg.so)
# 添加头文件路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/third_party/FFmpeg/include)
# 编译 NAPI 模块并链接依赖库
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so avcodec_ffmpeg)
2. 声明 ArkTS 接口
在 index.d.ts 中声明供 ArkTS 调用的方法,并通过 oh-package.json5 关联:
javascript
// entry/src/main/cpp/types/libentry/index.d.ts
export const callNativeFFmpeg: (inputPath: string, outputPath: string) => number;
二、 NAPI 接口桥接与 C++ 业务实现
NAPI 的核心职责是处理 ArkTS 与 C++ 之间的数据类型转换,并将实际业务委托给 C++ 类处理。
1. NAPI 接口注册与参数解析
在 napi_init.cpp 中注册方法,解析 ArkTS 传入的参数,并调用 C++ 业务类:
cpp
// napi_init.cpp
#include "napi/native_api.h"
#include "ffmpeg_utils.h" // 引入 C++ 业务逻辑头文件
static napi_value CallNativeFFmpeg(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);
// 解析 ArkTS 传入的字符串参数
char inputPath[1024], outputPath[1024];
size_t length;
napi_get_value_string_utf8(env, args[0], inputPath, sizeof(inputPath), &length);
napi_get_value_string_utf8(env, args[1], outputPath, sizeof(outputPath), &length);
// 调用 C++ 核心处理逻辑
int result = FfmpegUtils::processVideo(inputPath, outputPath);
// 返回结果给 ArkTS
napi_value res;
napi_create_int32(env, result, &res);
return res;
}
// 模块初始化与接口映射
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{ "callNativeFFmpeg", nullptr, CallNativeFFmpeg, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
2. C++ 核心业务逻辑(以 FFmpeg 视频裁剪为例)
在 C++ 层直接调用 FFmpeg 的 API 处理多媒体数据:
cs
// ffmpeg_utils.cpp
#include "ffmpeg_utils.h"
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include "hilog/log.h"
int FfmpegUtils::processVideo(const char* in_filename, const char* out_filename) {
AVFormatContext *ifmt_ctx = nullptr, *ofmt_ctx = nullptr;
int ret = 0;
// 1. 初始化输入输出上下文
ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0);
if (ret < 0) return ret;
ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
if (ret < 0) goto end;
// 2. 创建输出流并拷贝编码参数
for (int i = 0; i < (int)ifmt_ctx->nb_streams; i++) {
AVStream *out_stream = avformat_new_stream(ofmt_ctx, NULL);
avcodec_parameters_copy(out_stream->codecpar, ifmt_ctx->streams[i]->codecpar);
out_stream->codecpar->codec_tag = 0;
}
// 3. 打开输出文件,写入头信息,读取帧并写入...
ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
avformat_write_header(ofmt_ctx, NULL);
// (此处省略 av_read_frame 和 av_interleaved_write_frame 的循环处理逻辑)
end:
// 4. 释放资源
if (ifmt_ctx) avformat_close_input(&ifmt_ctx);
if (ofmt_ctx) avformat_free_context(ofmt_ctx);
return ret;
}
三、PixelMap 与 OpenCV 的内存交互
在图像处理中,ArkTS 侧通常使用 PixelMap,需要在 C++ 侧将其零拷贝或安全拷贝转换为 OpenCV 的 cv::Mat 进行处理。
cpp
// 方案:使用 OH_PixelMap_AccessPixels 获取内存地址并转换为 cv::Mat
#include <multimedia/image_framework/image_pixel_map_mdk.h>
#include <opencv2/opencv.hpp>
static napi_value AccessToMat(napi_env env, napi_callback_info info) {
// 1. 获取 ArkTS 传入的 NativePixelMap 对象
NativePixelMap *native = OH_PixelMap_InitNativePixelMap(env, args[0]);
// 2. 获取图像基本信息(宽高、行大小等)
struct OhosPixelMapInfos pixelMapInfos;
OH_PixelMap_GetImageInfo(native, &pixelMapInfos);
// 3. 获取内存地址并锁定
void *pixel = nullptr;
OH_PixelMap_AccessPixels(native, &pixel);
// 4. 构造 cv::Mat (注意传入 rowSize 保证内存对齐)
cv::Mat originMat(pixelMapInfos.height, pixelMapInfos.width, CV_8UC4, pixel, pixelMapInfos.rowSize);
// 5. 使用 OpenCV 进行图像处理(如灰度转换)
cv::Mat grayMat;
cv::cvtColor(originMat, grayMat, cv::COLOR_BGRA2GRAY);
// 6. 处理完毕后释放内存锁
OH_PixelMap_UnAccessPixels(native);
return nullptr;
}
核心注意事项
- 线程安全 :NAPI 的回调默认在 ArkTS 线程执行,如果 C++ 层有耗时操作(如视频编解码),务必使用
napi_create_async_work将其放入后台线程执行,避免阻塞 UI。 - SONAME 规范 :引入预构建的
.so库时,必须确保其设置了正确的 SONAME,否则在 HAP 打包运行时动态加载器可能无法找到依赖库。 - 内存管理:在 C++ 与 ArkTS 交互(尤其是传递 PixelMap 或 ArrayBuffer)时,务必遵循"谁申请谁释放"的原则,防止内存泄漏。
四、 工程化提效:IDE 自动化生成胶水代码
在复杂的 C++ 项目中,手动编写 napi_get_cb_info、napi_create_string_utf8 等类型转换代码极其繁琐且容易出错。DevEco Studio 提供了强大的跨语言代码编辑能力:
- 自动生成 NAPI 框架 :在 C++ 头文件(
.h/.hpp)中,将光标置于需要暴露给 ArkTS 的函数或类名上,右键选择Generate > NAPI,IDE 会自动生成对应的胶水代码框架及napi_init.cpp注册逻辑。 - 反向生成 C++ 定义 :在 ArkTS 侧的
.d.ts接口文件中,针对未实现的 Native 函数,点击悬浮提示的Generate native implementation,可一键生成对应的 C++ 函数定义,大幅缩短开发周期。
1、 自动生成 NAPI 框架(C++ 头文件 -> 注册与胶水代码)
假设你在 C++ 头文件中定义了一个简单的数学运算函数。
1. 编写 C++ 头文件
javascript
// entry/src/main/cpp/test.h
#ifndef TEST_H
#define TEST_H
int add(int a, int b);
int getNumber();
#endif // TEST_H
2. 触发 IDE 自动生成
- 首次生成 :确保当前 C++ 工程
entry > src > main > cpp路径下没有napi_init.cpp文件。将光标放置在add或getNumber函数名上,右键选择Generate > NAPI。IDE 会自动生成napi_init.cpp文件。 - 追加生成 :如果
napi_init.cpp已存在,再次对getNumber函数右键选择Generate > NAPI,IDE 会自动在napi_property_descriptor数组中追加注册信息,并生成对应的胶水代码框架。
3. IDE 自动生成的 napi_init.cpp 代码示例
cpp
// entry/src/main/cpp/napi_init.cpp
#include "napi/native_api.h"
#include "test.h"
// 自动生成的胶水代码框架
static napi_value NAPI_Global_add(napi_env env, napi_callback_info info) {
// ToDo: implements the code;
return nullptr;
}
static napi_value NAPI_Global_getNumber(napi_env env, napi_callback_info info) {
// ToDo: implements the code;
return nullptr;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
// 自动完成接口映射与注册
napi_property_descriptor desc[] = {
{ "NAPI_Global_add", nullptr, NAPI_Global_add, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "NAPI_Global_getNumber", nullptr, NAPI_Global_getNumber, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
2、 跨语言快速生成函数定义(ArkTS .d.ts -> C++ 实现)
在 ArkTS 侧声明好接口后,可以直接让 IDE 反向生成对应的 C++ 底层实现。
1. 在 ArkTS 侧声明接口
cpp
// entry/src/main/cpp/types/libentry/Index.d.ts
export const add: (a: number, b: number) => number;
export const test: (a: number, b: number) => number;
2. 触发 IDE 自动生成
将光标悬浮在未定义的函数名(如 test)处,IDE 会提示 Declared function 'test' has no native implementation.。此时点击页面出现的红色灯泡图标 ,或者按下快捷键 Alt + Shift + Enter,选择 Generate native implementation。
3. IDE 自动生成的 C++ 函数定义
IDE 会根据 .d.ts 中的类型签名,自动在对应的 C++ 文件中生成符合 NAPI 规范的函数定义:
cs
// 自动生成的 C++ 函数定义
static napi_value Test(napi_env env, napi_callback_info info) {
// ToDo: implements the code;
return nullptr;
}
五、 架构解耦:C++ 类的面向对象导出
在实际业务中,底层 C++ 库往往是以"类(Class)"的形式提供能力的(如 dlca_player 播放器类)。如果仅导出全局函数,会导致状态管理混乱。NAPI 支持将 C++ 类完整导出为 ArkTS 侧的类:
- 实例映射表管理 :在 C++ 侧维护一个全局的
std::map<int, NativeClass*>映射表。创建实例时,返回一个id给 ArkTS 侧;后续 ArkTS 调用方法时,传入id,C++ 侧通过id找到对应的 Native 对象并执行方法。 - 类定义注册 :通过
napi_define_class和napi_define_properties将 C++ 类的构造函数及成员方法(如play,stop,setConfig)挂载到 ArkTS 的 exports 对象上,实现面向对象的跨语言调用。
1、 C++ 侧:定义类与 NAPI 桥接
1. 定义底层 C++ 类
javascript
// MyDemo.h
class MyDemo {
public:
MyDemo(std::string m_name);
~MyDemo();
std::string name;
int add(int a, int b);
};
2. 实现 NAPI 绑定与生命周期管理
在 hello.cpp 中,通过 napi_define_class 建立 ArkTS 类与 C++ 类的映射,并在构造函数中使用 napi_wrap 绑定对象:
cpp
// hello.cpp
#include "napi/native_api.h"
#include "MyDemo.h"
// 1. ArkTS 侧的构造函数映射
static napi_value JsConstructor(napi_env env, napi_callback_info info) {
napi_value jDemo = nullptr;
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, &jDemo, nullptr);
// 获取 ArkTS 传入的参数并创建 C++ 对象
char name[50];
size_t result = 0;
napi_get_value_string_utf8(env, args[0], name, sizeof(name), &result);
MyDemo *cDemo = new MyDemo(name);
// 【核心】将 C++ 对象包装到 ArkTS 对象上
// 同时注册析构回调:当 ArkTS 对象被 GC 回收时,自动销毁 C++ 对象,防止内存泄漏
napi_wrap(env, jDemo, cDemo, [](napi_env env, void *finalize_data, void *finalize_hint) {
MyDemo *cDemo = (MyDemo *)finalize_data;
delete cDemo;
cDemo = nullptr;
}, nullptr, nullptr);
return jDemo;
}
// 2. ArkTS 侧的成员方法映射(以 add 为例)
static napi_value JsAdd(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_value jDemo = nullptr;
napi_get_cb_info(env, info, &argc, args, &jDemo, nullptr);
// 【核心】从 ArkTS 对象中解包出 C++ 对象指针
MyDemo *cDemo = nullptr;
napi_unwrap(env, jDemo, (void **)&cDemo);
// 获取参数并调用 C++ 方法
int value0, value1;
napi_get_value_int32(env, args[0], &value0);
napi_get_value_int32(env, args[1], &value1);
int cResult = cDemo->add(value0, value1);
napi_value jResult;
napi_create_int32(env, cResult, &jResult);
return jResult;
}
// 3. 模块初始化与类注册
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
// 定义类的方法映射
napi_property_descriptor classProp[] = {
{"add", nullptr, JsAdd, nullptr, nullptr, nullptr, napi_default, nullptr}
};
napi_value jDemo = nullptr;
const char *jDemoName = "MyDemo";
// 【核心】定义 ArkTS 类与 C++ 构造函数的关联
napi_define_class(env, jDemoName, NAPI_AUTO_LENGTH, JsConstructor, nullptr,
sizeof(classProp) / sizeof(classProp[0]), classProp, &jDemo);
// 将类挂载到 exports 对象上导出
napi_set_named_property(env, exports, jDemoName, jDemo);
return exports;
}
EXTERN_C_END
2、 ArkTS 侧:声明接口与调用
1. 声明 .d.ts 接口文件
为了让 ArkTS 侧能够识别并具备代码提示,需要在 index.d.ts 中声明对应的类结构:
cpp
// index.d.ts
declare namespace testNapi {
class MyDemo {
constructor(name: string);
name: string;
add(a: number, b: number): number;
}
}
export default testNapi;
2. 业务代码调用
在 ArkTS 业务逻辑中,可以像使用普通 TypeScript 类一样,通过 new 关键字创建 Native 对象并调用方法:
cpp
// Index.ets
import testNapi from 'libentry.so';
// 创建 Native 对象
const demo = new testNapi.MyDemo('TestInstance');
// 调用 Native 方法
const result = demo.add(10, 20);
console.info(`Native 计算结果: ${result}`);
六、 极致性能:纯 Native 架构与异步任务
对于性能极度敏感的场景(如实时计步器、物理引擎模拟),推荐采用**"纯 Native 架构"**:
- 核心逻辑全下沉:将传感器订阅、数据过滤、算法计算全部放在 C++ 层完成,ArkTS 层仅作为 UI 渲染展示层。由于 C++ 做计算天然比脚本层快,且避免了频繁的跨语言通信开销,能显著提升帧率与响应速度。
- 异步工作线程(Async Work) :NAPI 的回调默认在 ArkTS 主线程执行。对于耗时操作,必须使用
napi_create_async_work将任务分发到后台线程池执行,处理完毕后再通过回调将结果安全地传回 ArkTS 侧,坚决杜绝阻塞 UI 线程导致的掉帧或 ANR。
七、 内存与资源安全管控
- 指针安全传递 :在 ArkTS 与 Native 之间传递复杂的 C++ 对象时,通常将其强转为
int64_t指针值进行传递。务必在 ArkTS 侧提供显式的destroy接口,在 C++ 侧清理全局映射表并释放内存,防止内存泄漏。 - 生命周期管理 :充分利用 NAPI 的
ScopeManager和ReferenceManager来管理napi_value和napi_ref的生命周期,确保在跨语言传递复杂对象(如 PixelMap、ArrayBuffer)时,底层内存不会被垃圾回收机制意外释放。