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 ...
随机推荐
- ssm中mapper注入失败的传奇经历
最近因为要测试一个功能,需要用最短的时间来启动服务,开启测试程序,但平常所用的框架中已经集成了各种三方的东西,想着那就再重新搭建一个最简单的ssm框架吧. 搭建可参考:简单ssm最新搭建 搭建过程并不 ...
- gulp使用详情 及 3.0到4.0的坑
项目的所有依赖都可以安装,每个都有详细的注释. const gulp = require('gulp'); const sass = require('gulp-sass'); const brows ...
- spark入门(四)日志配置
1 背景 在测试spark计算时,将作业提交到yarn(模式–master yarn-cluster)上,想查看print到控制台这是很难的,因为作业是提交到yarn的集群上,所以,去yarn集群上看 ...
- 爬取链家网租房图 使用ImagesPipeline保存图片
# 爬虫文件 # -*- coding: utf-8 -*- import scrapy import os from urllib import request from lianjia.items ...
- k8s学习 - 概念 - Pod
k8s学习 - 概念 - Pod 这篇继续看概念,主要是 Pod 这个概念,这个概念非常重要,是 k8s 集群的最小单位. 怎么才算是理解好 pod 了呢,基本上把 pod 的所有 describe ...
- 齐治运维堡垒机后台存在命令执行漏洞(CNVD-2019-17294)分析
基本信息 引用:https://www.cnvd.org.cn/flaw/show/CNVD-2019-17294 补丁信息:该漏洞的修复补丁已于2019年6月25日发布.如果客户尚未修复该补丁,可联 ...
- NOIP 2017 惊魂记
考完了NOIP三周后才开始补……然后又补了一周…… DAY -1: 晚上吃了一顿送行宴散伙饭,然后默默地看了一遍之前所有考试后写的题解,再读了几遍板子,然后和QTY一起和达哥又一次在外面谈了一个小时, ...
- vue组件之间的传值——中央事件总线与跨组件之间的通信($attrs、$listeners)
vue组件之间的通信有很多种方式,最常用到的就是父子组件之间的传值,但是当项目工程比较大的时候,就会出现兄弟组件之间的传值,跨级组件之间的传值.不可否认,这些都可以类似父子组件一级一级的转换传递,但是 ...
- mvc区分页面内请求判断是否是Html.action或Html.RenderAction请求
ControllerContext.IsChildAction 来判断,如果用Html.Action或Html.RenderAction方法,这个属性返回true,否则返回false
- [JavaWeb] Ubuntu下载eclipse for ee
进入网站进行下载 https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/2019- ...