外观
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++ 服务持有,必须显式取消或转移所有权。
十、练习与答案
- JS GC 和 C++ 引用计数有什么根本差异?
- 为什么 Native 异步回调必须保存 JS callback?
- 保存后为什么还必须释放?
- 如何处理场景关闭后的 Native 任务?
答案:
- GC 基于可达性自动回收,引用计数或手动管理基于明确所有权/计数。
- 防止 callback 在任务完成前被 GC。
- 否则 callback 闭包及其引用对象会长期存活。
- 取消任务、标记请求失效、检查回调环境并在结束时断开引用。
四种危险状态
| 状态 | JS Proxy | Native | 风险 |
|---|---|---|---|
| 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 仍需额外取消/释放。
实验步骤
- 启动任务后立即关闭页面,预期 stale。
- 重复启动两次,只有最后代次 accepted。
- 将页面对象放入对象池,验证 unuse 也取消。
- 在 Native 测试包用已有异步 API做同样操作。
- 记录回调来源、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 实现。
- JS GC 会自动释放 Native 对象吗? 不应假设。Native 可能有独立 owner/引用计数。
- Native 对象销毁后 JS Proxy 怎么办? 收到销毁通知/状态标记,拒绝后续调用并清除引用。
- 为什么 sessionId 比 isValid 更重要? 对象仍可能有效但业务会话已经变化。
- 解除循环引用的首选? 明确取消、生命周期 owner 和回调清空,而不是等待 GC 猜测。
十一、本课总结
text
跨语言生命周期必须同时管理 JS 可达性、Native 所有权、代理映射和异步回调。
任何一边单独正确,都不能保证整个系统安全。十二、下一课预告
text
Lesson078|Web 与 Native 的运行环境为什么不完全一致