Cocos2d-x开发中Ref内存管理
Ref类是Cocos2d-x根类,Cocos2d-x中的很多类都派生自它,例如,我们熟悉的节点类Node也派生自Ref。我们介绍Ref内存管理。
内存引用计数
Ref类设计来源于Cocos2d-iphone的CCObject类,在Cocos2d-x 2.x中也叫CCObject类。因此Ref类的内存管理是参考Objective-C手动管理引用计数(Reference Count)而设计的。
如图所示是内存引用计数原理示意图。
每个Ref对象都有一个内部计数器,这个计数器跟踪对象的引用次数,被称为“引用计数”(Reference Count,简称RC)。当对象被创建时候,引用计数为1。为了保证对象的存在,可以调用retain函数保持对象,retain会使其引用计数加1,如果不需要这个对象可以调用release函数,release使其引用计数减1。当对象的引用计数为0的时候,引擎就知道不再需要这个对象了,就会释放对象内存。
引用计数实例如图所示,我们在ObjA中使用new等操作创建了一个Ref对象,这时候这个对象引用计数为1。然后在OjbB中使用retain函数保持Ref对象,这时引用计数为2。再然后ObjA中调用release函数,这时引用计数为1。在ObjB中调用release函数,这时引用计数为0。这个时候Ref对象就会由引擎释放。
在Ref类中相关函数有:retain()、release()、autorelease()和getReferenceCount()。其中autorelease()函数与release()函数类似,它会延后使引用计数减1,autorelease()我们稍后再介绍。getReferenceCount()函数返回当前的引用计数。
自动释放池
我们先看看下面的代码片段。
- XmlParser * XmlParser::createWithFile(const char *fileName)
- {
- XmlParser *pRet = new XmlParser();
- // ①
- return pRet;
- }
上述代码XmlParser::createWithFile(const char *fileName)函数能够创建XmlParser对象指针并返回给调用者。根据我们前面介绍的C++使用new规则,在XmlParser::createWithFile函数中还应该释放对象的语句,如果没有,那么每次调用者调用XmlParser::createWithFile函数都会创建一个新对象,老的对象没有释放,就会造成内存泄漏。但是如果我们在第①行代码,添加释放语句pRet->release(),那么问题可能会更严重,返回的对象可能已经被释放,返回的可能是一个野指针。
自动释放池(AutoReleasePool)正是为此而设计,自动释放池也是来源于Objective-C,Cocos2d-x中维护AutoreleasePool对象,它能够管理即将释放的对象池。我们在第①可以使用pRet->autorelease()语句,autorelease()函数将对象放到自动释放池,但对象的引用计数并不马上减1,而是要等到一个消息循环结束后减1,如果引用计数为0(即,没有被其它类或Ref对象retain),则释放对象,在此之前对象并不会释放。
消息循环是游戏循环一个工作职责,消息循环说到底还是游戏循环,消息循环是接收事件,并处理事件。自动释放池的生命周期也是由消息循环管理的。如图所示,图中“圈圈”是消息循环周期,它的一个工作职责是维护自动释放池创建和销毁。每次为了处理新的事件,Cocos2d-x引擎都会创建一个新的自动释放池,事件处理完成后,就会销毁这个池,池中对象的引用计数会减1,如果这个引用计数会减0,也就是没有被其它类或Ref对象retain,则释放对象,否则这个对象不会释放,在这次销毁池过程中“幸存”下来,它被转移到下一个池中继续生存。
下面我们看一个实例,下面代码是13.2.2一节的实例HelloWorldScene.cpp代码:
- bool HelloWorld::init()
- {
- if ( !Layer::init() )
- {
- return false;
- }
- Size visibleSize = Director::getInstance()->getVisibleSize();
- Vec2 origin = Director::getInstance()->getVisibleOrigin();
- auto goItem = MenuItemImage::create(
- "go-down.png",
- "go-up.png",
- CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
- goItem->setPosition(Vec2(origin.x + visibleSize.width - goItem->getContentSize().width/2 ,
- origin.y + goItem->getContentSize().height/2));
- auto menu = Menu::create(goItem, NULL); ①
- menu->setPosition(Vec2::ZERO);
- this->addChild(menu, 1); ②
- this->list = __Array::createWithCapacity(MAX_COUNT);
- this->list->retain(); ③
- for(int i = 0;i < MAX_COUNT; ++i){
- Sprite* sprite = Sprite::create("Ball.png");
- this->list->addObject(sprite); ④
- }
- return true;
- }
- void HelloWorld::menuCloseCallback(Ref* pSender)
- {
- Ref* obj = NULL;
- log("list->count() = %d",this->list->count()); ⑤
- Size visibleSize = Director::getInstance()->getVisibleSize();
- CCARRAY_FOREACH(this->list, obj) {
- Sprite* sprite = (Sprite*)obj; ⑥
- int x = CCRANDOM_0_1() * visibleSize.width;
- int y = CCRANDOM_0_1() * visibleSize.height;
- sprite->setPosition( Vec2(x, y) );
- this->removeChild(sprite);
- this->addChild(sprite);
- }
- }
- HelloWorld::~HelloWorld()
- {
- this->list->removeAllObjects();
- CC_SAFE_RELEASE_NULL(this->list); ⑦
- }
在上述代码中我们需要关注两个对象(Menu和__Array)创建。第①行auto menu = Menu::create(goItem, NULL)通过create静态工厂创建对象,关于静态工厂的创建原理我们会在下一节介绍。如果我们不采用第②行的this->addChild(menu, 1)语句将menu 对象放入到当前层(HelloWorld)的子节点列表中,那么这个menu对象就会在当前消息循环结束的时候被释放。调用this->addChild(menu, 1)语句会将它的生命周期持续到HelloWorld层释放的时候,而不会在当前消息循环结束释放。
菜单、层等节点对象可以调用addChild函数,使得其生命延续。而且__Array和__Dictionary等Ref对象没有调用addChild函数保持,我们需要显式地调用retain函数保持它们,以便延续其生命。如代码第③行this->list->retain(),list就是一个__Array指针类型的成员变量,如果没有第③行语句,那么在第⑤行代码this->list->count()程序就会出错,因为这个时候list对象已经释放了。采用了retain保持的成员变量,一定要release(或autorelease),retain和release(或autorelease)一定是成对出现的。我们可以在析构函数~HelloWorld()中调用release释放,而第⑦行代码CC_SAFE_RELEASE_NULL(this->list)就是实现这个目的,其中CC_SAFE_RELEASE_NULL宏作用如下:
list->release();
list = nullptr;
可见CC_SAFE_RELEASE_NULL宏不仅仅释放对象,还将它的指针设置为nullprt[],也样可以防止野指针。
上述代码还有一个非常重要的关于内存的问题,我们在HelloWorld::init()函数中创建了很多Sprite对象,通过第④行代码this->list->addObject(sprite)将它们放到list容器对象中,它们没有调用addChild函数也没有显式retain函数,然而在当前消息循环结束后它们还是“存活”的,所以在第⑥行Sprite* sprite = (Sprite*)obj中,取出的对象是有效的。这个原因__Array和__Dictionary等容器对象的add相关函数可以使添加对象引用计数加1,相反的remove相关函数可以使添加对象引用计数减1。
Ref内存管理规则
下面我们给出使用Ref对象时候,内存管理一些基本规则:
1、在使用Node节点对象时候,addChild函数可以保持Node节点对象,使引用计数加1。通过removeChild函数移除Node节点对象,使引用计数减1。它们都是隐式调用的,我们不需要关心它们的内存管理。这也正是为什么在前面的章节中我们无数次地使用了Node节点对象,而从来都没有担心过它们内存问题。
2、如果是__Array和__Dictionary等容器对象,可以通过它们add相关函数添加元素会使引用计数加1,相反的remove相关函数删除元素会使引用计数减1。但是前提是__Array和__Dictionary等容器对象本身不没有被释放。
3、如果不属于上面提到的Ref对象,需要保持引用计数,可以显式调用retain函数使引用计数加1,然后显式调用release(或autorelease)函数使引用计数减1。
4、每个 retain函数一定要对应一个 release函数或一个 autorelease函数。
5、release函数使得对象的引用计数马上减1,这是所谓的“斩立决”,但是是否真的释放掉内存要看它的引用计数是否为0。autorelease函数只是在对象上做一个标记,等到消息循环结束的时候再减1,这是所谓的“秋后问斩”,在“秋天”没有到来之前,它的内存一定没有释放,可以安全使用,但是“问斩”之后,是否真的释放掉内存要看它的引用计数是否为0。因此无论是那一种方法,引用计数是为0才是释放对象内存的条件。下面的代码是Ref类的release函数,通过这段代码可以帮助我们理解引用计数。
- void Ref::release()
- {
- CCASSERT(_referenceCount > 0, "reference count should greater than 0");
- --_referenceCount;
- if (_referenceCount == 0)
- {
- #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
- auto poolManager = PoolManager::getInstance();
- if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
- {
- // Trigger an assert if the reference count is 0 but the Ref is still in autorelease pool.
- // This happens when 'autorelease/release' were not used in pairs with 'new/retain'.
- //
- // Wrong usage (1):
- //
- // auto obj = Node::create(); // Ref = 1, but it's an autorelease Ref which means it was in the autorelease pool.
- // obj->autorelease(); // Wrong: If you wish to invoke autorelease several times, you should retain `obj` first.
- //
- // Wrong usage (2):
- //
- // auto obj = Node::create();
- // obj->release(); // Wrong: obj is an autorelease Ref, it will be released when clearing current pool.
- //
- // Correct usage (1):
- //
- // auto obj = Node::create();
- // |- new Node(); // `new` is the pair of the `autorelease` of next line
- // |- autorelease(); // The pair of `new Node`.
- //
- // obj->retain();
- // obj->autorelease(); // This `autorelease` is the pair of `retain` of previous line.
- //
- // Correct usage (2):
- //
- // auto obj = Node::create();
- // obj->retain();
- // obj->release(); // This `release` is the pair of `retain` of previous line.
- CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
- }
- #endif
- delete this;
- }
- }
6、一个对象调用autorelease函数,它就会将对象放到自动释放池里,它生命周期自动释放池生命周期息息相关,池在消息循环结束的时候会释放,池会调用池内对象release函数,使得它们的引用计数减1。下面的代码是AutoreleasePool类的清除函数clear(),通过这段代码可以帮助我们理解自动释放池机制。
- void AutoreleasePool::clear()
- {
- #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
- _isClearing = true;
- #endif
- for (const auto &obj : _managedObjectArray)
- {
- obj->release();
- }
- _managedObjectArray.clear();
- #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
- _isClearing = false;
- #endif
- }
本书交流讨论网站:http://www.cocoagame.net
《Cocos2d-x实战 C++卷》现已上线,各大商店均已开售:
京东:http://item.jd.com/11584534.html
当当:http://product.dangdang.com/23606265.html
互动出版网:http://product.china-pub.com/3770734
《Cocos2d-x实战 C++卷》源码及样章下载地址:
源码下载地址:http://51work6.com/forum.php?mod=viewthread&tid=1155&extra=page%3D1
样章下载地址:http://51work6.com/forum.php?mod=viewthread&tid=1157&extra=page%3D1
Cocos2d-x开发中Ref内存管理的更多相关文章
- Unity游戏开发中的内存管理_资料
内存是手游的硬伤——Unity游戏Mono内存管理及泄漏http://wetest.qq.com/lab/view/135.html 深入浅出再谈Unity内存泄漏http://wetest.qq.c ...
- Cocos2d-x开发中C++内存管理
由于开始并没有介绍C++语言,C++的内存管理当然也没进行任何的说明,为了掌握Cocos2d-x中的内存管理机制,是有必要先了解一些C++内存管理的知识.C++内存管理非常复杂,如果完全地系统地介绍可 ...
- iOS开发中的内存管理
一.为什么要进行内存管理 系统资源有限,iOS会为每一个执行的程序分配30M的内存,超过20M会收到内存警告,超过30M将会终止应用程序.因此,要及时回收一些不须要再继续使用的内存空间,比方回收一些不 ...
- JNI中的内存管理(转)
源:JNI中的内存管理 JNI 编程简介 JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互 ...
- Android中的内存管理机制以及正确的使用方式
概述 从操作系统的角度来说,内存就是一块数据存储区域,属于可被操作系统调度的资源.现代多任务(进程)的操作系统中,内存管理尤为重要,操作系统需要为每一个进程合理的分配内存资源,所以可以从两方面来理解操 ...
- Python 中的内存管理
Python 中一切皆对象,这些对象的内存都是在运行时动态地在堆中进行分配的,就连 Python 虚拟机使用的栈也是在堆上模拟的.既然一切皆对象,那么在 Python 程序运行过程中对象的创建和释放就 ...
- C++中的内存管理
在C++中也是少不了对内存的管理,在C++中只要有new的地方,在写代码的时候都要想着delete. new分配的时堆内存,在函数结束的时候不会自动释放,如果不delete我分配的堆内存,则会造成内存 ...
- cocos2dx中的内存管理机制及引用计数
1.内存管理的两大策略: 谁申请,谁释放原则(类似于,谁污染了内存,最后由谁来清理内存)--------->适用于过程性函数 引用计数原则(创建时,引用数为1,每引用一次,计数加1,调用结束时, ...
- 6.关于QT中的内存管理,动态的制作,动态库的调用,静态库的制作
一 QT的内存管理 1 QT中的内存管理是QObject来管理的 2 QT中的内存管理没有cocos2dx中的引用计数 3 组件能够指定父对象 QTimer *timer = QTime ...
随机推荐
- U3D中的协同等待函数
WaitForSeconds.WaitForseconds等待函数; 创建一个yield指令,来等待给定的秒数; using UnityEngine; using System.Collections ...
- SRV记录说明
SRV记录是DNS服务器的数据库中支持的一种资源记录的类型,它记录了哪台计算机提供了哪个服务这么一个简单的信息 SRV 记录:一般是为Microsoft的活动目录设置时的应用.DNS可以独立于活动 ...
- BZOJ 1607: [Usaco2008 Dec]Patting Heads 轻拍牛头 筛法
1607: [Usaco2008 Dec]Patting Heads 轻拍牛头 Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://www.lyds ...
- 也谈读书和书籍选择问题(C#)
前言 读到一篇.net程序员应该看什么书?深有感触.以前曾经用C#也开发过几年的东西.在那里对相关语言和开发都有了一定的了解.这里,结合自己当初的一些体会和见识把一些比较好的书籍也和大家分享一下.这一 ...
- xampp
Fatal error: Class 'kernel' not found in C:\xampp\htdocs\shopex\install\install.core.php on line 10 ...
- .net平台下C#socket通信(转)
上篇.net平台下C#socket通信(上)介绍了socket通信的基本原理及最基本的通信方式.本文在此基础上就socket通信时经常遇到的问题做一个简单总结,都是项目中的一些小问题,拿来此处便于下次 ...
- 实例源码--Android小工具源码
下载源码 技术要点: 1. Android控件布局的使用 2. Http通信 3. XML数据解析 4. 网络状态的监听 5. 源码带有非常详细的中文注释 ...... 详细介绍: 1. An ...
- SVN 修改URL路径
http://strugglelinux.blog.51cto.com/1009905/672008 标签:休闲 SVN 修改URL路径 职场 原创作品,允许转载,转载时请务必以超链接形式标明文章 原 ...
- The First
好吧,今天是第一次,写博客,虽然有点不明白,博客的作用.以后就写点感想,与每天的收获.既然,在博客园注册,自然也与IT相关了. hello,world!
- Android进阶笔记12:Manymo(在线安卓系统模拟器工具)
Manymo: 在线安卓系统模拟器工具是一款启动速度快,且在浏览器中就能运行流畅.你可以使用它来测试你的安卓应用,他最多能支持42种屏幕尺寸和系统版本. 长久以来,Android开发者面临的困境之一就 ...