写点什么

HarmonyOS NAPI 实战避坑:如何避免 napi_value 悬挂指针问题

  • 2026-08-03
    北京
  • 本文字数:2470 字

    阅读完需:约 8 分钟

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:HarmonyOS NAPI实战避坑:如何避免 napi_value 悬挂指针问题-华为开发者话题 | 华为开发者联盟

在鸿蒙应用开发中,不知道你有没有遇到过这种“灵异事件”:

明明刚才还活蹦乱跳的 JS 对象,转眼间就“人间蒸发”了。你的代码只是轻轻摸了一下它的属性,应用就“啪”地一声,崩溃了。

日志里抛出一行冷冰冰的异常:

ReferenceError: Can not get Prototype on non ECMA Object
复制代码

翻译成人话就是:“我拿到的这个东西,根本就不是个正经的 JS 对象,连原型都没有,你让我怎么操作?”

如果你曾为此抓耳挠腮,别急。今天我们就来揭开这个错误背后的“真凶”——悬空的 napi_value 句柄。它就像一个披着对象外衣的幽灵,看着像那么回事,一碰就碎。

一、根因:你把“临时工”当“正式工”用了

在 HarmonyOS 的 NAPI 交互中,napi_value 是一个很特殊的角色。它本质上是一个局部句柄,类似于一个“临时工”工牌。它的有效生命周期,仅限于当前函数调用栈和 HandleScope(作用域)之内。

一旦这个作用域结束(比如函数返回),或者垃圾回收(GC)被触发,这个“临时工”工牌就会被收回。如果你非要拿着这个过期的工牌去访问原本对应的 JavaScript 对象,引擎就会告诉你:“我不认识这个人。”

最常见的翻车姿势,就是把 napi_value 直接扔进 Native 层的全局变量里存着,想着下次再拿出来用。

这就好比你把临时工的门禁卡偷偷复制了一份,结果第二天门禁系统更新了,你拿着复制卡去刷——门没开,人被抓了。

二、案发现场:一次点击,两次崩溃

我们来看一个真实的“惨案”:

ArkTS 侧代码很干净:

// 先添加一个数组testNapi.add(2, 3);  // 手动触发垃圾回收(模拟内存紧张)ArkTools.hintGC();  // 再获取那个数组的长度const len = testNapi.getvalue().length;  // 💥 崩溃点
复制代码

Native 侧(C++)是这么写的“有 Bug”的代码:

// 全局变量,赤裸裸地存了一个 napi_valuenapi_value g_globalArray = nullptr;napi_value Add(napi_env env, ...) {    napi_value jsArray;    napi_create_array(env, &jsArray);  // 建一个数组    // ... 往数组里塞数据 ...    g_globalArray = jsArray;  // ❌ 直接赋值给全局,危险!    return someResult;}napi_value GetValue(napi_env env, ...) {    return g_globalArray;  // ❌ 返回一个可能已死的句柄}
复制代码

崩溃过程还原:

1. 调用 add 时,在 HandleScope 内创建了一个数组 jsArray,并顺手把它赋给了全局变量 g_globalArray。

2. 函数结束,HandleScope 关闭,jsArray 的引用计数减少,它变成了“可被回收”的状态。

3. 紧接着 ArkTS 侧调用 ArkTools.hintGC(),垃圾回收器启动,那个数组对象被物理回收,内存被释放。

4. 但 g_globalArray 还傻傻地保存着原来那块内存的地址——现在它成了一个悬空指针

5. 最后调用 GetValue 返回这个地址,并在 ArkTS 侧访问.length。引擎一看,这块内存里根本不是 ECMA 对象,于是怒抛 Can not get Prototype on non ECMA Object。

三、破案神器:把“临时工”转正

问题的核心就是:局部句柄不能直接长期持有。正确的做法是,给它办一张“正式工”工牌——也就是 napi_ref(持久引用)

修复方案非常简单,把全局变量类型改成 napi_ref,并在创建对象后立即创建引用:

// 全局变量改为 napi_ref 类型napi_ref g_globalArrayRef = nullptr;// 创建对象时napi_value jsArray;napi_create_array(env, &jsArray);// 为 jsArray 创建持久引用,引用计数 +1,防止被 GC 回收napi_create_reference(env, jsArray, 1, &g_globalArrayRef);// 获取对象时napi_value GetValue(napi_env env, ...) {    napi_value result = nullptr;    napi_get_reference_value(env, g_globalArrayRef, &result);    return result;  // 安全返回}// 当确定不再需要时(如模块卸载),必须释放引用// napi_delete_reference(env, g_globalArrayRef);
复制代码

这样一来,只要 napi_ref 还在,垃圾回收器就不会动你的对象。你拿到的是一个活生生的 JavaScript 数组,访问.length 自然稳稳当当。

四、还有哪些“雷区”?

除了全局变量存裸 napi_value,以下场景同样危险,请务必警惕:

• 跨异步回调传递:在异步任务(如 napi_create_async_work)中,把主线程的 napi_value 塞到上下文里,等异步完成后再去用——早已失效。

• 多线程共享:不同线程间直接传递 napi_value,它本身不是线程安全的,就算没被回收,也可能引发内存破坏。

• HandleScope 嵌套混乱:在函数内打开了多个作用域,提前关闭了外层,导致内部创建的对象被提前释放。

五、几条保命锦囊

1、黄金法则:凡是要存下来的,一律用 napi_ref;凡是临时用的,用 napi_value。

o 存全局 → 用 ref

o 存异步回调 → 用 ref

o 存成员变量 → 用 ref

2、异步场景三板斧:

o 发起前创建 napi_ref

o 回调中通过 napi_get_reference_value 取出使用

o 用完立即 napi_delete_reference,避免内存泄漏

3、代码审查口诀:看到 static napi_value 或 global napi_value 立刻亮红灯,要求改为 napi_ref。

4、善用工具:在编译期或 CI 中增加静态检查,扫描这类危险赋值,把问题消灭在萌芽。

写在最后

NAPI 开发就像在 JavaScript 和 C++之间搭桥,桥的两端风景不同,规则也不同。

记住一句话:napi_value 是“借”来的,用完得还;napi_ref 是“买”下来的,归你了才能安心存

掌握好这个生命周期管理,你就能彻底告别“Can not get Prototype”这类幽灵崩溃,让应用跑得又稳又长久。

如果这篇文章帮你解决了一个困扰已久的难题,欢迎点个「在看」或分享给团队里的伙伴,大家一起远离悬空指针,拥抱稳稳的幸福😊

(文中代码示例仅为演示核心逻辑,实际开发请结合完整异常处理和资源释放。)


🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5

🔗 社区 DFX 专题文章: https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001

【扫码加入 HarmonyOS DFX 技术交流群】