Skip to content

Stage06|JSB & Native Bridge ​

Lesson077|GC、引用计数与跨语言对象生命周期 ​

Tags: #Creator2.x #Stage06 #GC #ReferenceCount #LifetimeDifficulty: ⭐⭐⭐⭐⭐


真实问题:下载完成回调偶发崩溃,重现率却很低 ​

Native 下载器持有回调,JavaScript 页面关闭后代理被 GC;下载线程完成时仍尝试调用旧回调。另一条路径又因为 JS 持有代理,Native 对象已提前销毁。

跨语言生命周期至少要同时画两条线:

text
JS 可达性 / GC
C++ 引用计数 / owner

它们不会自动同步。

一、本课目标 ​

JavaScript 和 C++ 使用不同的内存管理模型。跨语言对象最危险的问题是:

一边认为对象还活着,另一边已经把它回收或销毁了。

本课理解 GC、引用计数、Root/保存引用、代理失效和回调清理。


二、两边的生命周期模型 ​

JavaScript ​

text
对象可达
→ 保留

对象不可达
→ 等待 GC
→ 回收

C++ ​

可能使用:

text
手动 new / delete
引用计数
RAII / 智能指针
引擎对象池或延迟销毁

绑定层必须协调这两套模型,不能只相信其中一边。


三、四种危险状态 ​

text
1. JS 活着,C++ 已销毁
2. C++ 活着,JS 已 GC
3. 两边都活着,但映射失效
4. 两边互相引用,谁也无法释放

可能表现为:

  • Native 崩溃。
  • 回调访问无效对象。
  • 场景切换后内存不降。
  • 同一个对象重复包装或重复释放。

四、为什么需要保存 JS 回调 ​

C++ 异步任务可能稍后回调 JavaScript:

text
JS 创建任务和 callback
→ Native 保存 callback
→ JS 当前函数结束
→ 稍后 Native 完成
→ 调用 callback

如果 callback 没有被正确保存,它可能在完成前被 GC;如果永久保存却不释放,又会泄漏。

正确设计需要:

text
任务开始时保存
→ 完成、取消或失败时释放
→ ScriptEngine 退出时统一清理

五、C++ 对象销毁后代理怎么办 ​

Native 对象销毁时,绑定层应让 JS 代理进入明确的无效状态,避免继续把旧指针当成有效对象。

text
C++ 对象销毁
→ 清除 Native-to-JS 映射
→ 使 JS Proxy 失效
→ 后续调用返回错误或被阻止

业务代码仍应检查对象生命周期,不要依赖无效代理“碰巧没有崩溃”。


六、循环引用 ​

text
JS Object
→ 保存 Native Proxy
→ Native Object 保存 JS Callback
→ Callback 闭包又引用 JS Object

这可能形成跨语言循环。单看 JS Heap 或 C++ 引用计数都不容易发现完整链路。

解决方向:

  • 明确所有者。
  • 回调使用弱引用或任务生命周期。
  • 取消时断开两边引用。
  • 避免全局容器永久保存代理。

七、场景切换中的生命周期 ​

text
旧 Scene 退出
→ Node / Component destroy
→ JS 对象等待 GC
→ Native 资源延迟释放
→ 异步任务可能仍在运行

所以场景切换时需要:

  • 取消下载、定位、支付等原生任务或转交所有权。
  • 取消回调和事件。
  • 清理代理映射。
  • 避免回调重新引用旧场景。

八、案例:Native 下载完成后崩溃 ​

时间线:

text
页面启动下载
→ 用户关闭页面
→ JS Component 销毁
→ 下载线程完成
→ Native 直接调用旧 JS Callback
→ 崩溃或异常

改进:

text
任务拥有 requestId
页面退出取消或标记失效
Native 完成后切回脚本线程
检查 ScriptEngine 和 callback
只交付给当前有效请求
任务结束释放 callback

九、常见误区 ​

误区一:JavaScript GC 会自动释放所有 C++ 对象 ​

两边需要绑定层和所有权协议协调。

误区二:C++ 引用计数为 0 后 JS 代理仍可安全使用 ​

代理必须失效,业务也不能继续访问。

误区三:保存 callback 越久越安全 ​

永久保存会让 JS 对象和闭包无法回收。

误区四:场景销毁后原生异步任务自然停止 ​

任务可能由平台或 C++ 服务持有,必须显式取消或转移所有权。


十、练习与答案 ​

  1. JS GC 和 C++ 引用计数有什么根本差异?
  2. 为什么 Native 异步回调必须保存 JS callback?
  3. 保存后为什么还必须释放?
  4. 如何处理场景关闭后的 Native 任务?

答案:

  1. GC 基于可达性自动回收,引用计数或手动管理基于明确所有权/计数。
  2. 防止 callback 在任务完成前被 GC。
  3. 否则 callback 闭包及其引用对象会长期存活。
  4. 取消任务、标记请求失效、检查回调环境并在结束时断开引用。

四种危险状态 ​

状态JS ProxyNative风险
A活活正常
B活已销毁失效句柄
C不可达活回调/资源泄漏
D循环持有循环持有永不释放

解决方案不是让一边永远持有,而是定义 owner、借用期、销毁通知和取消路径。

Creator 2.4.x 生命周期实验:用可取消任务模拟两边 ​

NativeLikeTask.ts ​

ts
const { ccclass } = cc._decorator;

@ccclass
export default class NativeLikeTask extends cc.Component {
    private generation = 0;
    private active = false;

    startTask() {
        const id = ++this.generation;
        this.active = true;

        this.scheduleOnce(() => {
            if (id !== this.generation || !this.active) {
                cc.warn('[native-like] stale callback', id);
                return;
            }
            cc.log('[native-like] callback accepted', id);
        }, 1);
    }

    cancelTask() {
        this.generation++;
        this.active = false;
    }

    onDisable() {
        this.cancelTask();
    }
}

这不是 C++ 引用计数实现,而是用公开 Creator 生命周期复现“任务完成晚于 owner”的提交规则。真实 Native API 仍需额外取消/释放。

实验步骤 ​

  1. 启动任务后立即关闭页面,预期 stale。
  2. 重复启动两次,只有最后代次 accepted。
  3. 将页面对象放入对象池,验证 unuse 也取消。
  4. 在 Native 测试包用已有异步 API做同样操作。
  5. 记录回调来源、session、owner 与错误。

循环引用不只发生在 JS ​

text
JS Proxy → callback closure → Native owner
Native owner → JS callback

如果 Native 只在回调结束后释放,而回调又等待 Native 销毁通知,可能形成循环。打断方式:

  • 明确 unsubscribe/cancel;
  • Native 不强持有长期 UI;
  • JS 回调完成后清空;
  • owner 结束时主动断环;
  • 回调携带弱/代次语义。

具体 weak reference 能力和桥接实现要按平台验证。

真实项目故障:退出账号后旧下载写入新账号 ​

账号 A 的下载任务还在,用户退出并登录 B。A 的结果回调返回,写入全局缓存,B 读到错误资源。

修复:

text
账号会话生成 sessionId
→ 任务保存 sessionId
→ 回调比较当前 session
→ 不匹配则丢弃/取消
→ 缓存键包含用户/内容版本

不能只判断页面 Node 是否有效,因为全局服务仍然有效。

GC 与引用计数的边界 ​

GC 管理 JS 可达图;C++ 引用计数管 Native owner 关系。一个对象可能:

  • JS 不可达但 Native 因回调仍活;
  • JS 可达但 Native handle 无效;
  • 两侧互相持有形成泄漏;
  • 资源由第三方系统持有。

调试时分别输出两侧状态,不把 JS Heap Snapshot 当 Native 内存报告。

版本边界与练习答案 ​

Creator 2.4.x 的 JS Engine、JSB 绑定和平台异步 API生命周期不同。具体 retain/release、回调线程和销毁通知需看当前 Native 实现。

  1. JS GC 会自动释放 Native 对象吗? 不应假设。Native 可能有独立 owner/引用计数。
  2. Native 对象销毁后 JS Proxy 怎么办? 收到销毁通知/状态标记,拒绝后续调用并清除引用。
  3. 为什么 sessionId 比 isValid 更重要? 对象仍可能有效但业务会话已经变化。
  4. 解除循环引用的首选? 明确取消、生命周期 owner 和回调清空,而不是等待 GC 猜测。

十一、本课总结 ​

text
跨语言生命周期必须同时管理 JS 可达性、Native 所有权、代理映射和异步回调。
任何一边单独正确,都不能保证整个系统安全。

十二、下一课预告 ​

text
Lesson078|Web 与 Native 的运行环境为什么不完全一致