本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

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_value
napi_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”这类幽灵崩溃,让应用跑得又稳又长久。

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

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

----------------------------------------------------------------------------------------------------------

      🔗 官网开发者学堂视频:华为开发者学堂

     🔗 社区DFX专题文章:  华为开发者问答 | 华为开发者联盟

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

Logo

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

更多推荐