【HarmonyOS开发小实践】Node-API 的SO 命名规则、多线程限制与调试

上一篇咱们把 Node-API 的基本流程走通了。但工程化用起来还会撞上一堆"为什么这样不行"的问题:so 名字改了加载不到、Native 函数里开个线程回调 ArkTS 就 crash、模拟器跑得好好的真机崩了。这篇把 SO 命名、注册、多线程、调试、性能这几块的约束和背后原因讲清楚,省得踩坑靠玄学。

SO 命名规则

ArkTS 侧 import xxx from 'libentry.so' 这一行,背后对应三个地方的名字必须对齐:

位置 写法 取值
CMakeLists.txt add_library(entry SHARED ...) entry
napi_init.cpp demoModule.nm_modname = "entry" "entry"
ArkTS 侧 import xxx from 'libentry.so' libentry.so

规则是:模块名为 entry,so 文件名就是 libentry.so,nm_modname 字段填 entry(不带 lib 前缀和 .so 后缀),ArkTS import 时写完整的 libentry.so。大小写也要一致,Entry 和 entry 是两个不同的模块。

这个对应关系不是凑巧,是加载链路硬性要求的:ArkTS 引擎拿到 import 'libentry.so',去文件系统找 libentry.so,dlopen 加载;so 加载时跑 RegisterDemoModule,把 nm_modname = "entry" 注册到模块表;ArkTS 引擎再根据 import 路径反查模块表,找到对应 exports 对象。任何一环名字不对,要么 so 找不到,要么模块注册了但查不到,结果都是 undefined。

多模块工程里,每个 Native 模块都要有自己的 so 名,不能图省事都用 entry。比如有 libcrypto.so 和 libimage.so 两个模块,nm_modname 分别是 "crypto" 和 "image",注册入口函数名也要不一样(RegisterCryptoModule、RegisterImageModule),否则符号冲突。

注册建议

Init 函数加 static

cpp 复制代码
static napi_value Init(napi_env env, napi_value exports) { ... }

这个 static 不是可有可无的。C++ 里函数默认有外部链接,没 static 的话符号会导出到 so 的符号表,两个 so 都有 Init 函数时,链接器可能把调用解析到错误的那个上。加了 static 把符号限制在文件内,就不会串。

注册入口函数名唯一

cpp 复制代码
extern "C" __attribute__((constructor)) void RegisterDemoModule() { ... }

extern "C" 去掉 C++ name mangling,__attribute__((constructor)) 让 so 加载时自动调用。这个函数名必须全工程唯一。两个 so 都导出 RegisterDemoModule 符号,dlopen 第二个 so 时可能覆盖第一个的符号,导致第一个模块没被注册。

实际工程里建议按模块名命名:RegisterCryptoModule、RegisterImageModule,一眼看出归属,也不容易重名。

重复注册不会生效

同一个 so 被多次 import,napi_module_register 会被调用多次,但模块表里只保留第一次注册的结果。所以不用担心 ArkTS 侧多个文件 import 同一个 so 导致重复注册。

多线程限制

这是 Node-API 工程化里最容易出问题的地方。规则只有一条,但后果很严重:

每个引擎实例对应一个 ArkTS 线程,实例上的对象不能跨线程操作,否则 crash。

拆开看:

  • Node-API 接口只能在 ArkTS 线程用 :所有 napi_* 函数都假设调用方在创建 env 的那个线程。在 Native 自己开的 std::thread 里调 napi_create_double(env, ...),env 是从 ArkTS 线程拿的,跨线程用,行为未定义,大概率 crash。
  • env 和线程绑定 :napi_env 不是个无状态句柄,它背后是当前线程的引擎上下文。同一个 env 不能在两个线程用。
  • napi_value 不能跨线程持有 :一个线程创建的 napi_value 在另一个线程里是无效的,GC 可能已经把它回收了。

#mermaid-svg-iGpWIiVLqlSJGZ1T{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iGpWIiVLqlSJGZ1T .error-icon{fill:#552222;}#mermaid-svg-iGpWIiVLqlSJGZ1T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iGpWIiVLqlSJGZ1T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .marker.cross{stroke:#333333;}#mermaid-svg-iGpWIiVLqlSJGZ1T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iGpWIiVLqlSJGZ1T p{margin:0;}#mermaid-svg-iGpWIiVLqlSJGZ1T .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .cluster-label text{fill:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .cluster-label span{color:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .cluster-label span p{background-color:transparent;}#mermaid-svg-iGpWIiVLqlSJGZ1T .label text,#mermaid-svg-iGpWIiVLqlSJGZ1T span{fill:#333;color:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .node rect,#mermaid-svg-iGpWIiVLqlSJGZ1T .node circle,#mermaid-svg-iGpWIiVLqlSJGZ1T .node ellipse,#mermaid-svg-iGpWIiVLqlSJGZ1T .node polygon,#mermaid-svg-iGpWIiVLqlSJGZ1T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .rough-node .label text,#mermaid-svg-iGpWIiVLqlSJGZ1T .node .label text,#mermaid-svg-iGpWIiVLqlSJGZ1T .image-shape .label,#mermaid-svg-iGpWIiVLqlSJGZ1T .icon-shape .label{text-anchor:middle;}#mermaid-svg-iGpWIiVLqlSJGZ1T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .rough-node .label,#mermaid-svg-iGpWIiVLqlSJGZ1T .node .label,#mermaid-svg-iGpWIiVLqlSJGZ1T .image-shape .label,#mermaid-svg-iGpWIiVLqlSJGZ1T .icon-shape .label{text-align:center;}#mermaid-svg-iGpWIiVLqlSJGZ1T .node.clickable{cursor:pointer;}#mermaid-svg-iGpWIiVLqlSJGZ1T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .arrowheadPath{fill:#333333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iGpWIiVLqlSJGZ1T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iGpWIiVLqlSJGZ1T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iGpWIiVLqlSJGZ1T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iGpWIiVLqlSJGZ1T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .cluster text{fill:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T .cluster span{color:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iGpWIiVLqlSJGZ1T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iGpWIiVLqlSJGZ1T rect.text{fill:none;stroke-width:0;}#mermaid-svg-iGpWIiVLqlSJGZ1T .icon-shape,#mermaid-svg-iGpWIiVLqlSJGZ1T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iGpWIiVLqlSJGZ1T .icon-shape p,#mermaid-svg-iGpWIiVLqlSJGZ1T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iGpWIiVLqlSJGZ1T .icon-shape .label rect,#mermaid-svg-iGpWIiVLqlSJGZ1T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iGpWIiVLqlSJGZ1T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iGpWIiVLqlSJGZ1T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iGpWIiVLqlSJGZ1T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Native子线程
ArkTS线程
异步派发
ArkTS 调 Native 函数
CallNative 执行

env 在此线程有效
返回 napi_value
std::thread 启动
在此线程调 napi_*

❌ env 跨线程

想跨线程怎么办

Native 侧有耗时计算想开线程做,做完了想通知 ArkTS,不能直接拿 env 调 napi_call_function。正确做法是:

  1. Native 子线程里把数据算完,结果存成 C++ 原生类型(int、std::string 这种,不是 napi_value)。
  2. 通过线程安全的队列把结果传回 ArkTS 线程。
  3. ArkTS 线程从队列取结果,在 ArkTS 线程里调 napi_create_* 包成 napi_value 返回。

或者用 napi_threadsafe_function(TSFN)这个专门为跨线程回调设计的接口。它创建一个线程安全的函数句柄,任意线程都能往里塞调用请求,引擎在 ArkTS 线程上串行执行。TSFN 的 API 比较繁琐,但跨线程回调 ArkTS 这是唯一靠谱的路。

一个错误示例

cpp 复制代码
// 错误:在 Native 子线程里用 env 调 ArkTS callback
static napi_value BadAsyncCall(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    std::thread([env, args]() {
        // ❌ env 和 args[0] 都属于 ArkTS 线程,这里用会 crash
        napi_value argv;
        napi_create_int32(env, 42, &argv);
        napi_value result;
        napi_call_function(env, nullptr, args[0], 1, &argv, &result);
    }).detach();

    return nullptr;
}

这段代码在子线程里用 env 创建 napi_value 并调用 callback,运行时大概率 crash,且堆栈看不出明显原因,因为 crash 发生在 GC 或引擎内部状态被破坏之后。这种 bug 极难定位,必须从源头避免。

代码调试

设备选择

设备 适合场景 注意
真机 性能调优、Native crash 复现 优先选
模拟器 无真机或无权限时 部分 Native 行为和真机有差异
预览器 ❌ 不能调 Native 只渲染组件,不加载 so

预览器调 Native 会报 TypeError: undefined is not callable,因为预览器根本没 dlopen so。这个报错看起来像 ArkTS 写错了,其实是预览器不支持,换模拟器或真机就好。

真机和模拟器差异主要在 CPU 架构(真机 arm64,模拟器 x86_64)和系统库版本。涉及 ABI 细节(如结构体对齐、调用约定)的 Native 代码,模拟器过了真机不一定过,最终验证得在真机。

日志调试

Native 侧打日志用 __hiwrite_log(HiLog):

cpp 复制代码
#include "hilog/log.h"

#define LOG_TAG "MyNative"
#define LOG_DOMAIN 0x3200

static napi_value CallNative(napi_env env, napi_callback_info info) {
    // ... 业务逻辑
    OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "CallNative result: %{public}f", sum);
    return result;
}

ArkTS 侧的 console.log 和 Native 侧的 HiLog 都能在 hilog 流里看到。Native crash 的堆栈在 crash 发生后用 hdc shell hilog | grep -A 30 "SIGSEGV" 抓。

断点调试

DevEco Studio 支持双端断点:ArkTS 和 C++ 都能下断点,调试时在同一个 session 里来回切。配置好 Native C++ 工程后,Debug 模式运行,ArkTS 调进 Native 时会自动切到 C++ 调试视图,变量、调用栈都能看。

混合调试的前提是 so 带 debug 符号(CMake 里 set(CMAKE_BUILD_TYPE Debug) 或 RelWithDebInfo),release 模式 strip 过符号的 so 断点下不准。

性能注意事项

跨边界开销

每次 ArkTS 调 Native 都有固定开销:

  • 参数从 napi_value 转成 C++ 类型(napi_get_value_*)
  • 调用进入 Native 上下文
  • 返回值从 C++ 类型包回 napi_value(napi_create_*)
  • 引擎状态切换

单次调用开销在微秒级,看着不大,但在循环里调几十万次就明显了。

typescript 复制代码
// 慢:循环里反复跨边界
let sum = 0;
for (let i = 0; i < 1000000; i++) {
  sum = nativeModule.add(sum, i);
}

// 快:一次跨边界,C++ 里循环
let sum = nativeModule.rangeSum(0, 1000000);

对应的 C++:

cpp 复制代码
static napi_value RangeSum(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);

    int64_t start, end;
    napi_get_value_int64(env, args[0], &start);
    napi_get_value_int64(env, args[1], &end);

    int64_t sum = 0;
    for (int64_t i = start; i < end; i++) {
        sum += i;  // 纯 C++ 循环,不跨边界
    }

    napi_value ret;
    napi_create_int64(env, sum, &ret);
    return ret;
}

实测百万次加法,循环跨边界版本要几十毫秒,一次跨边界版本不到 1 毫秒。差距全在边界开销上。

数据转换成本

不同类型转换成本差很多:

类型 转换方式 成本
number (int/double) napi_get_value_double 低
boolean napi_get_value_bool 低
string 两次调用拿长度+拷贝 中
Object 逐字段 napi_get_named_property 高
ArrayBuffer napi_get_arraybuffer_info 拿指针 极低

传大块数值数据用 ArrayBuffer,Native 侧直接拿到内存指针操作,没有拷贝:

cpp 复制代码
static napi_value ProcessArray(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    void* data = nullptr;
    size_t length = 0;
    napi_get_arraybuffer_info(env, args[0], &data, &length);

    // 直接操作内存,零拷贝
    double* arr = static_cast<double*>(data);
    size_t n = length / sizeof(double);
    for (size_t i = 0; i < n; i++) {
        arr[i] = arr[i] * 2.0;  // 原地翻倍
    }
    return nullptr;
}

ArkTS 侧:

typescript 复制代码
const buf = new ArrayBuffer(8 * 1000);  // 1000 个 double
const view = new Float64Array(buf);
for (let i = 0; i < 1000; i++) view[i] = i;
nativeModule.processArray(buf);  // 原地修改

图像处理、矩阵运算这种场景,ArrayBuffer 是标配。

举个栗子:Native 侧计时器回调 ArkTS

需求:ArkTS 调 Native 启动一个定时器,N 秒后 Native 回调 ArkTS 通知。子线程里不能直接用 env,得用 TSFN。

cpp 复制代码
#include <thread>
#include <chrono>

static napi_value StartTimer(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 delayMs;
    napi_get_value_double(env, args[0], &delayMs);

    // 创建 TSFN:把 ArkTS callback 包装成线程安全句柄
    napi_threadsafe_function tsfn;
    napi_value workName;
    napi_create_string_utf8(env, "TimerCallback", NAPI_AUTO_LENGTH, &workName);
    napi_create_threadsafe_function(
        env, args[1], nullptr, workName, 0, 1, nullptr, nullptr, nullptr,
        [](napi_env env, napi_value /*cb*/, void* /*context*/, void* data) {
            // 在 ArkTS 线程执行
            napi_value cb = static_cast<napi_value>(data);
            napi_value undefined;
            napi_get_undefined(env, &undefined);
            napi_value result;
            napi_call_function(env, undefined, cb, 0, nullptr, &result);
        },
        &tsfn);

    // 子线程里等待,然后通过 TSFN 触发回调
    std::thread([tsfn, delayMs]() {
        std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(delayMs)));
        // Acquire 一次,把 callback 推进队列
        napi_acquire_threadsafe_function(tsfn);
        napi_call_threadsafe_function(tsfn, nullptr, napi_tsfn_nonblocking);
        napi_release_threadsafe_function(tsfn, napi_tsfn_release);
    }).detach();

    return nullptr;
}

ArkTS 侧:

typescript 复制代码
import nativeModule from 'libentry.so';

nativeModule.startTimer(2000, () => {
  console.log('2 秒到了');
});

TSFN 的 API 看着繁琐,但套路固定:创建时传一个"在 ArkTS 线程执行的回调",子线程里 napi_call_threadsafe_function 把数据塞进去,引擎在 ArkTS 线程上串行调用那个回调。所有跨线程回调 ArkTS 的场景都是这个模式。

注意一下下

  • so 改了不生效 :CMake 缓存问题。DevEco 里 Build > Clean Project,再重新 build。或者直接删 entry/build/default 目录。
  • crash 在 OS_GC_Thread 线程 :堆栈里看到 OS_GC_Thread、CompressGCMarker 这类字样,多半是 Native 侧把 napi_value 跨线程持有,GC 移动对象时指针失效。回查 Native 代码里有没有把 napi_value 存到 C++ 全局变量或子线程闭包里。
  • 模拟器跑通真机崩 :检查 ABI 相关代码。结构体对齐、long 大小(arm64 是 8 字节,x86_64 也是 8,但 x86 是 4)、未定义行为(UB)在不同架构表现不同。Sanitize 工具能帮着抓 UB。
  • import 报 undefined :so 没编出来,或名字不对。先看 entry/build 下有没有 libentry.so,再看 CMake 里 add_library 名字和 nm_modname 对不对。
  • Native 侧内存泄漏 :用 napi_create_* 创建的 napi_value 由 GC 管,不用手动释放。但 Native 侧自己 malloc/new 的内存要自己释放,引擎不会代劳。napi_create_external 创建的 external 对象可以挂 finalize 回调,对象被 GC 时回调里释放 Native 内存。

总结一下下

  • 跨边界调用次数是性能关键指标,不是单次调用快慢。把 N 次小调用合并成 1 次大调用,性能能差几个数量级。
  • TSFN 是跨线程回调的唯一正规路径。别想着用全局变量存 env 然后子线程里调,迟早 crash。
  • Native 侧的 C++ 异常不会自动传到 ArkTS,try/catch 在 ArkTS 侧抓不到。Native 侧要么自己 catch 完返回错误码,要么用 napi_throw_error 抛 ArkTS 异常。
  • 线上 crash 排查靠 hilog 抓堆栈,但 release 包符号被 strip 了,堆栈是地址。要保留一份带符号的 so 做离线符号化,DevEco build 出来的 entry/build/default/obj/default/entry/ 下有未 strip 的 so。
相关推荐
LucianaiB2 小时前
用 HarmonyOS 做一张会写诗的月夜明信片:追月的完整开发复盘
华为·ai·harmonyos·skill
蒸鱼Yuzheng3 小时前
HarmonyOS HAP 与调试工件治理:包结构、版本身份与自动化证据链
自动化·性能测试·数据治理·harmonyos·hap
轻口味3 小时前
HarmonyOS 7 新特性3:TiledGSNode——轻带看让 71MB 庭院按视口按需加载:真机实测与零请求降级
华为·harmonyos·鸿蒙·tiledgsnode
李游Leo4 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》01:从空场景到第一个可运行的3D房间【鸿蒙心迹】
3d·华为·harmonyos
猛犸象限4 小时前
【共创稿事节】关卡进了源码,却对不上 JSON——回归不是再截一张图
harmonyos
用户29469405448164 小时前
鸿蒙真机调试三板斧:Playwright 为什么驱动不了 Electron-on-鸿蒙
harmonyos·deepseek
木子雨廷4 小时前
第 26 天|Worker 多线程:重任务不阻塞 UI
harmonyos
huainingning4 小时前
MSTP技术及华三华为锐捷MSTP配置对比
华为
AI备忘录5 小时前
(二十二)华为华三锐捷迈普思科 802.1X 端口认证配置命令(网络准入五厂商对照)
运维·服务器·开发语言·网络·安全·华为