Cocos2d-x v3.11 中的新内存模型
Cocso2d-x v3.11 一项重点改进就是 JSB 新内存模型。这篇文章将专门介绍这项改进所带来的新研发体验和一些技术细节。
1. 成果
在 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
2. 新内存模型所解决的问题
让我们回到问题本身,之前的内存模型导致问题的根本原因在于:JSB 中的一个 Cocos 对象实际上同时对应一个 C++ 层的 Native 对象和一个脚本层的 JS 对象,而这两个对象的生命周期不完全同步。在 JSB 引擎中有如此设计的原因在于,JSB 的核心层执行在 C++ 中,JS 层提供的是用户接口,为了让用户的 JS 对象接口可以影响到核心层的执行,我们通过 JS 绑定技术维护了 C++ 对象和 JS 对象的一一映射关系,让 JS 对象的接口可以通过绑定层转发给 C++ 层。
而两种对象生命周期的不同步,会引发前文所提到的各种难以调试的问题:
- Invalid Native Object:JS 对象在脚本层仍然被持有,但是其对应的 C++ 对象已经被释放。典型的案例是用removeFromParent 将节点移除出场景,此时 C++ 对象将会被释放,而 JS 对象索引如果仍然被持有,是可以访问的,但是调用任何绑定层提供的接口,都会发现无法找到 C++ 层对象而崩溃。
- 脚本对象丢失:与上一条情况相反,C++ 对象仍然存在,而与它关联的 JS 对象已经被垃圾回收机制回收。这种情况往往可以归因于绑定层没有正确得持有 JS 对象,较为罕见,可以视为绑定层的 bug。
新的内存模型尝试从根本上解决这个问题:同步原生对象和脚本对象的生命周期。
3. 研发历程
其实内存问题从 JSB 诞生之日就存在,解决它的过程经历了几个重要的节点:
- 从 2014 年我们就开始尝试解决这个问题,不过当时遇到了一些 Spidermonkey 脚本引擎中的技术难题未能彻底解决,被搁置。
- 去年底重开这个课题的研究,在切换了几次思路后,终于有了解决方案的雏形。
- Cocos2d 创始人,也是我们的总架构师 Ricardo 介入,从基础上对 JSB 绑定层代码进行了重构,完成了绑定接口的抽象,避免直接使用 Spidermonkey 接口。也在此基础上提供了一种新的解决方案。
- 绑定接口的抽象被合并入 Cocos2d-x v3.9。
- 在 v3.10 中,通过对绑定层的完整检查,我们基本解决了脚本对象丢失的问题。
- 通过多轮测试并稳定后的新内存模型在默认关闭的情况下被合并入 v3.11。
可以看出这套解决方案并不是一蹴而就完成的,它经历了多次迭代和基础框架的重构,我们不能保证它是完美的,但我们很负责任得在做这件事情。如果开发者们遇到任何问题,请反馈给我们,我们会持续迭代,争取让这套新内存模型可靠稳定得运行在用户的 JS 游戏中,并降低游戏的崩溃率,提升开发效率。
4. 基本原理
让我们先看看 v3.10 中的绑定层是如何工作的:
这张图展示了一个游戏的场景树在 JSB 中的实际内存结构,左半部分是原生层的 C++ 对象,右半部分是脚本层的 JS 对象。可以看到每个节点以两份对象同时存在于原生层和脚本层,如此设计的原因是:
- 为了让引擎尽可能高效,我们将大多数函数的实现放在了原生层,由原生对象来执行,其编译后的效率远高于 JS。
- 同时,为了让这些接口可以在 JS 层被调用,让用户感受到无缝的 JS 编程体验,原生对象的壳实际上是一个 JS 对象,它的 API 接口被桥接到原生实现上。
基于这样的设计,我们在绑定层保存了原生对象和脚本对象的双向映射关系,然而这还不够,我们还需要保障原生对象和脚本对象生命周期的一致性。在原生层,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 的索引,尽管脚本层仍然不知道这个索引的意义是什么,但简单的索引足够解决上面的问题了。
至此,游戏环境中完整的引用关系已经暴露给脚本层的垃圾回收机制,所以依赖垃圾回收机制来控制脚本对象和原生对象的生命周期可以认为是可靠的。
5. 总结
以上就是 v3.11 中新内存模型的基本原理,它能够在绝大多数情况下避免原生对象和脚本对象生命周期不同步的问题。这个方案的核心思路有两点:
- 使用垃圾回收机制同时控制原生对象和脚本对象的生命周期
- 传递原生层的引用关系(比如父子节点引用)给脚本层
当然,新的内存模型也有一个难以避免的问题,那就是它的内存占用往往比旧的版本更高,这点取决于游戏中的内存管理做得如何。所以在 v3.11 中我们同时提供了两种内存模型,可以使用 CC_ENABLE_GC_FOR_NATIVE_OBJECTS 宏来进行切换,默认情况下,引擎使用的是旧内存模型。
对于开发者们,我们给的建议是,如果是已经发布了原生版本的成熟游戏,并且没有遇到对象生命周期引起的崩溃问题,那么可以继续使用旧的内存模型。对于下面的这些情况,我们建议使用新内存模型:
- 新开发的游戏
- 开发者对于 Cocos2d-x 中的内存模型不熟悉
- 开发者对于 C++ 开发不熟悉
- 已有项目中深受 Invalid Native Object 之苦
Cocos2d-x v3.11 中的新内存模型的更多相关文章
- c++11 standardized memory model 内存模型
C++11 标准中引入了内存模型,其目的是为了解决多线程中可见性和顺序(order).这是c++11最重要的新特征,标准忽略了平台的差异,从语义层面规定了6种内存模型来实现跨平台代码的兼容性.多线程代 ...
- java中JVM虚拟机内存模型详细说明
java中JVM虚拟机内存模型详细说明 2012-12-12 18:36:03| 分类: JAVA | 标签:java jvm 堆内存 虚拟机 |举报|字号 订阅 JVM的内部结构 ...
- 全网最硬核 Java 新内存模型解析与实验单篇版(不断更新QA中)
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...
- 一起学习c++11——c++11中的新语法
c++11新语法1: auto关键字 c++11 添加的最有用的一个特性应该就是auto关键字. 不知道大家有没有写过这样的代码: std::map<std::string, std::vect ...
- 理论与实践中的 C# 内存模型
转载自:https://msdn.microsoft.com/magazine/jj863136 这是该系列(包含两部分内容)的第一部分,这部分将以较长的篇幅介绍 C# 内存模型. 第一部分说明 C# ...
- 理论与实践中的 C# 内存模型,第 2 部分
转载自:https://msdn.microsoft.com/zh-cn/magazine/jj883956.aspx 这是介绍 C# 内存模型的系列文章的第二篇(共两篇). 正如在 MSDN 杂志十 ...
- Akka系列(四):Akka中的共享内存模型
前言...... 通过前几篇的学习,相信大家对Akka应该有所了解了,都说解决并发哪家强,JVM上面找Akka,那么Akka到底在解决并发问题上帮我们做了什么呢? 共享内存 众所周知,在处理并发问题上 ...
- SQLSERVER2014中的新功能
SQLSERVER2014中的新功能 转载自:http://blog.csdn.net/maco_wang/article/details/22701087 博客人物:maco_wang SQLSER ...
- C++11 中值得关注的几大变化(网摘)
C++11 中值得关注的几大变化(详解) 原文出处:[陈皓 coolshell] 源文章来自前C++标准委员会的 Danny Kalev 的 The Biggest Changes in C++11 ...
随机推荐
- WebGL概述
WebGL,是一项用来在网页上绘制和渲染复杂三维图形(3D图形),并允许用户与之交互的技术.WebGL基于OpenGL ES 2.0,使用GLSL ES语言编写着色器.而 OpenGL ES (Ope ...
- client-go中的golang技巧
client-go中有很多比较有意思的实现,如定时器,同步机制等,可以作为移植使用.下面就遇到的一些技术讲解,首先看第一个: sets.String(k8s.io/apimachinery/pkg/u ...
- Arrays工具类常用方法演示
java.util.Arrays是JDK中操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法. 下面我们以int类型数组为例,学习下常用的方法,其他类型数组都差不多. 1.equals( ...
- feign服务端出异常客户端处理的方法
在使用feign进行远程方法调用时,如果远程服务端方法出现异常,客户端有时需要捕获,并且把异常信息返回给前端,而如果在开启熔断之后,这个异常会被消化,所以说,如果希望拿到服务端异常,feign.hys ...
- 关于ArrayList的扩容机制
关于ArrayList的扩容机制 ArrayList作为List接口常用的一个实现类,其底层数据接口由数组实现,可以保证O(1) 复杂度的随机查找, 在增删效率上不如LinkedList,但是在查询效 ...
- 删除git中缓存的用户名和密码
我们使用Git命令去clone Gitlab仓库的代码时,第一次弹框提示输入账号密码的时候输错了,然后后面就一直拒绝,不再重复提示输入账号密码,怎么破? git报错信息 运行一下命令缓存输入的用户名和 ...
- 2018年东北地区赛S - Problem I. Spell Boost HDU - 6508
题目地址:https://vjudge.net/problem/HDU-6508 思路:给一些卡,分为四种卡.1.白卡(没效果)2.魔法,作用卡(会对作用卡的费用减少,也会被魔法卡作用)3.作用卡(会 ...
- c++学习书籍推荐《深入理解C++11 C++11新特性解析与应用》下载
百度云及其他网盘下载地址:点我 编辑推荐 <深入理解C++11:C++11新特性解析与应用>编辑推荐:C++标准委员会成员和IBM XL编译器中国开发团队共同撰写,权威性毋庸置疑.系统.深 ...
- 数据结构与算法---线索化二叉树(Threaded BinaryTree)
先看一个问题 将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树 问题分析: 当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 } 但是 6, 8 ...
- SpringCloud-Alibaba-Sentinel(1)初探
Sentinel 是什么? Sentinel 具有以下特征: 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围) ...