标签:过程 tom src 编程 object 控制 bag 做了 完全
在 Cocos2d-x v3.11 之前的版本中,使用 JS 语言发布原生版本的用户可能多少都会遇到一个经典的问题:Invalid Native Object,或者遇到一些莫名其妙的 JS 对象失效的崩溃。而解决这些问题,我们给出的解决方案基本是使用 retain / release 来显式声明持有或释放对象,或者是在脚本层更合理得持有对象索引。而在 v3.11 中,用户不再需要担心这些问题,新的内存模型会更合理得控制原生对象和 JS 对象的生命周期,基本让 C++ 层的对象对用户透明化,不再需要考虑它的存在。
可以说,启用新内存模型后,用户可能根本不会感受到它,但它切实得为用户减少了问题的产生,让开发体验更流畅舒心。
我们针对新内存模型做了很多的测试,目前没有发现任何问题,但是为了避免影响成熟的用户项目,目前新内存模型默认是关闭的,你需要手动开启该功能。开启的方法是在 cocos/base/ccConfig.h 里把 CC_ENABLE_GC_FOR_NATIVE 的值改为1:
#ifdef CC_ENABLE_SCRIPT_BINDING
#ifndef CC_ENABLE_GC_FOR_NATIVE_OBJECTS
#define CC_ENABLE_GC_FOR_NATIVE_OBJECTS 1 // change to 1
#endif
#endif
让我们回到问题本身,之前的内存模型导致问题的根本原因在于:JSB 中的一个 Cocos 对象实际上同时对应一个 C++ 层的 Native 对象和一个脚本层的 JS 对象,而这两个对象的生命周期不完全同步。在 JSB 引擎中有如此设计的原因在于,JSB 的核心层执行在 C++ 中,JS 层提供的是用户接口,为了让用户的 JS 对象接口可以影响到核心层的执行,我们通过 JS 绑定技术维护了 C++ 对象和 JS 对象的一一映射关系,让 JS 对象的接口可以通过绑定层转发给 C++ 层。
而两种对象生命周期的不同步,会引发前文所提到的各种难以调试的问题:
新的内存模型尝试从根本上解决这个问题:同步原生对象和脚本对象的生命周期。
其实内存问题从 JSB 诞生之日就存在,解决它的过程经历了几个重要的节点:
可以看出这套解决方案并不是一蹴而就完成的,它经历了多次迭代和基础框架的重构,我们不能保证它是完美的,但我们很负责任得在做这件事情。如果开发者们遇到任何问题,请反馈给我们,我们会持续迭代,争取让这套新内存模型可靠稳定得运行在用户的 JS 游戏中,并降低游戏的崩溃率,提升开发效率。
让我们先看看 v3.10 中的绑定层是如何工作的:
这张图展示了一个游戏的场景树在 JSB 中的实际内存结构,左半部分是原生层的 C++ 对象,右半部分是脚本层的 JS 对象。可以看到每个节点以两份对象同时存在于原生层和脚本层,如此设计的原因是:
基于这样的设计,我们在绑定层保存了原生对象和脚本对象的双向映射关系,然而这还不够,我们还需要保障原生对象和脚本对象生命周期的一致性。在原生层,Cocos2d-x 使用引用计数机制来控制对象生命周期,而在脚本层则依赖 Spidermonkey 的垃圾回收机制。那么下面开始介绍 Cocos2d-x v3.10 和 v3.11 分别是怎么处理生命周期的。
回看上图,其中红色的箭头表示原生对象对脚本对象的引用,这个引用是在 Spidermonkey 中建立的,所以它可以保障原生对象存在时,脚本对象不会被释放。而反过来就不一定了,让我们看看下面的例子:
var scene = new cc.Scene();
cc.director.runScene(scene);
var sprite = new cc.Sprite(‘role.png‘);
setTimeout(function () {
// Crash !!! Invalid Native Object
scene.addChild(sprite);
}, 1000);
由于在创建好 sprite 之后,没有立即将它加入到场景中,所以 sprite 的引用计数会在当前帧将为 0 并被释放。而在脚本层,Spidermonkey 却很好得维护了 sprite 的索引,因为在 setTimeout 的回调函数中还引用了它。所以当调用 addChild 的原生层实现时,会发现找不到 sprite 的原生对象了,继而触发 Invalid Native Object 并崩溃。
而在 v3.11 的新内存模型中,我们反其道而行之,由脚本层对象持有原生对象的引用,而仅在脚本对象被垃圾回收的时候才释放原生对象。所以新内存模型也被称为 Full GC Relied Memory Model(完全依赖垃圾回收机制的内存模型)。通过下面这张图可以看到它的基本运作方式:
图中虚线代表脚本对象对原生对象的引用(通过增加引用计数),这样即便从节点树上删除某个节点,它的原生对象也不会被释放。而当脚本对象被垃圾回收的时候,会减少它所引用的原生对象的引用计数,使得原生对象也会被释放。
看起来似乎不会再出现恼人的 Invalid Native Object 了,但不知道大家注意没有,如果排除掉图中红色的箭头,其实只是 v3.10 的反向而已,那么会出现原生层对象还存在,但是脚本对象已经被释放的问题。参考下面的代码:
(function () {
var scene = new cc.Scene();
cc.director.runScene(scene);
var sprite = new cc.Sprite(‘role.png‘);
sprite.custom = ‘A custom property‘;
var TAG = 1;
scene.addChild(sprite, 1, TAG);
setTimeout(function () {
cc.sys.garbageCollect();
var sp = scene.getChildByTag(TAG);
// sp.custom will be undefined
cc.log(sp.custom);
}, 1000);
})();
这次在 setTimeout 的回调函数中,经过我们模拟调用垃圾回收,外部的 sprite 由于在 JS 层已经完全不可访问所以被释放了(闭包)。而它的原生对象还被 scene 所引用,所以从 scene 中是可以获取到的(这里涉及绑定层的一个设计,在原生对象对应的脚本对象不存在时,会主动创建一个新的脚本对象),但是已经和外部的 sprite 不是同一个对象了,所以无法获取像 custom 这样的任何自定义属性。
为了解决这个问题,我们将原生层的映射关系复制到了脚本层,也就是上图中红色的箭头部分。在调用 addChild 的时候,有一段特殊代码会给脚本层的 scene 添加一个指向 sprite 的索引,尽管脚本层仍然不知道这个索引的意义是什么,但简单的索引足够解决上面的问题了。
至此,游戏环境中完整的引用关系已经暴露给脚本层的垃圾回收机制,所以依赖垃圾回收机制来控制脚本对象和原生对象的生命周期可以认为是可靠的。
以上就是 v3.11 中新内存模型的基本原理,它能够在绝大多数情况下避免原生对象和脚本对象生命周期不同步的问题。这个方案的核心思路有两点:
当然,新的内存模型也有一个难以避免的问题,那就是它的内存占用往往比旧的版本更高,这点取决于游戏中的内存管理做得如何。所以在 v3.11 中我们同时提供了两种内存模型,可以使用 CC_ENABLE_GC_FOR_NATIVE_OBJECTS 宏来进行切换,默认情况下,引擎使用的是旧内存模型。
对于开发者们,我们给的建议是,如果是已经发布了原生版本的成熟游戏,并且没有遇到对象生命周期引起的崩溃问题,那么可以继续使用旧的内存模型。对于下面的这些情况,我们建议使用新内存模型:
标签:过程 tom src 编程 object 控制 bag 做了 完全
原文地址:https://www.cnblogs.com/lixiao24/p/10680102.html