跨语言的指针传递,NAPI 数组和字符串的内存边界
用 NAPI 写原生模块,最容易让人产生“我在写 C++”错觉的地方,是 ArkTS 和 C++ 之间的数据传递。在业务侧,传一个 10MB 的 Uint8Array 或者一串极长的文本给底层去处理,看似正常的函数调用,跑起来可能直接导致应用内存暴增甚至直接闪退。排查栈里抛出的往往是一个简单的 Out of memory,根本看不出是哪一行指针出了问题。
问题的核心在于:ArkTS 的垃圾回收机制(GC)和 C++ 的手动内存管理是两套平行的宇宙。当你在两端之间传递数据时,如果搞错了拷贝和引用的边界,就会引发内存泄漏,或者访问到已经被释放的野指针。

字符串传参的拷贝开销
在 C++ 侧接收一个 ArkTS 字符串,很多人直接用 napi_get_value_string_utf8。这里有个隐蔽的陷阱:如果不预先分配好足够长的 buffer,直接传 NULL,只能拿到字符串长度;而为了拿到长度去分两次调用的写法,会导致大字符串的解析时间翻倍。
cpp
size_t str_size = 0; napi_get_value_string_utf8(env, args[0], nullptr, 0, &str_size); char* buffer = new char[str_size + 1]; napi_get_value_string_utf8(env, args[0], buffer, str_size + 1, &str_size); std::string result(buffer); delete[] buffer;
这段代码看起来完整,但每次调用都会在 C++ 堆上分配一次内存,再拷贝一次。如果这种传参发生在高频循环(比如音视频帧处理或大模型推理的 Token 传递)中,内存分配和释放的碎片化会直接拖垮性能。
更严重的错误是忘了 delete[] buffer。每一次 ArkTS 侧的调用,都会在 Native 侧留下一段永不释放的内存。随着时间推移,内存曲线会呈完美的阶梯状上升。解决思路是用栈分配,或者把 buffer 的申请频率降下来,比如使用局部静态变量或内存池。
ArrayBuffer 的真共享与假共享
传输二进制数据,ArrayBuffer 是最常用的形态。但传递 ArrayBuffer 时,必须明确你是要拷贝一份,还是共享同一块内存。
如果使用 napi_get_arraybuffer_info 拿到指针直接操作,这是共享。C++ 修改这块内存,ArkTS 侧能立刻看到。但这要求 C++ 代码绝对不能在这块内存被使用期间去释放它,因为它的生命周期是归 ArkTS 的 GC 管的。
cpp
void* data = nullptr; size_t length = 0; napi_get_arraybuffer_info(env, args[0], &data, &length); // 此时 data 指向 ArkTS 管理的物理内存 process_data(data, length);
如果某个后台线程正在读取这个指针,而 ArkTS 侧把所有的引用置空了,GC 触发后直接回收了这段内存,后台线程就会读取到一段非法地址,触发崩溃。
如果 C++ 必须长时间持有这块数据,就要用 napi_create_external_arraybuffer 或者显式拷贝。
cpp
void* copied_data = malloc(length); memcpy(copied_data, data, length); // 将 copied_data 扔给异步线程,用完自己 free
拷贝的代价是内存翻倍,不拷贝的代价是随时可能出现的野指针。在安全面前,对于跨线程长时间处理的数据,老老实实拷贝才是稳妥的选择。不要为了省一点内存,把整个进程的稳定性搭进去。
引用计数与句柄泄漏
另一个极其常见的坑,是 napi_ref 的滥用。为了在异步回调里把结果传回 ArkTS 侧,往往需要将 ArkTS 对象存成引用。但存储之后,如果在回调结束时忘记了释放,这就是一个实打实的内存泄漏。
cpp
napi_ref callback_ref; napi_create_reference(env, callback, 1, &callback_ref);
在异步任务完成,回调结束后,务必要调用:
cpp
uint32_t result = 0;
napi_reference_unref(env, callback_ref, &result);
if (result == 0) {
napi_delete_reference(env, callback_ref);
}
这是标准的引用计数释放流程。忘记这一步,不仅回调函数本身,连同回调捕获的所有闭包变量,都会死死地挂在内存里。在 HiChecker 或者 Native 泄漏检测工具里,这种泄漏的堆栈通常会指向 NAPI 的注册函数。
跨语言内存管理的边界约束
跨语言边界的开发,核心准则是分清“谁分配,谁释放”。ArkTS 传过来的对象,C++ 只管用,不要试图去 free 它;C++ 自己 new 出来的对象,如果要交给 ArkTS 侧接管,必须用 napi_create_external 系列接口把释放逻辑注册进去。不要在 NAPI 里随手写个 delete,那通常意味着一场灾难。
更多推荐

所有评论(0)