必须尊重作者:
版权声明:本文由( 小塔 )原创,转载请保留文章出处!
本文链接:http://www.zaojiahua.com/memory-management.html
今天看了一下cocos2dx的内存管理机制,有些地方不太好理解搞了挺长的时间,现在感觉自己理解的差不多了,赶快写下自己的思路和互联网的广大朋友分享,如果你发现有错误的地方或者不理解的地方欢迎指正!首先我们必须说一下c++中变量的内存空间的分配问题,我们在c++中写一个类,可以在栈上分配内存空间也可以使用new在堆上分配内存空间,如果类对象是在栈上分配的内存空间,这个内存空间的管理就不是我们的事了,但如果是在堆上分配的内存空间,当然需要我们来手动的delete了!cocos2dx采用的是在堆上分配内存空间,想想看你在写程序的时候对于cocos2dx中的类是不是大多数都是通过工厂方法获得的一个指针,你见过在栈上分配内存空间的情况吗?所以问题来了,既然在堆上分配内存空间,那么如何管理这个内存空间,什么时候应该释放就是个问题了!在程序中,当我们创建了一个对象的时候,这块内存空间经常是被不同的对象引用,如果删除的早了,有对象还在引用这块内存空间那么程序必然要崩溃!所以cocos2dx引入了引用计数这个内存管理机制。
当我们在堆上分配一块内存空间的时候,这个对象的引用计数就是1,当有对象要引用这块内存空间的时候,这个引用计数就增加1,当有对象不再引用这块内存的时候引用计数就减1,当这个引用计数减为0的时候就使用delete删除掉这块内存,这样就做到了当有对象引用的时候会正常的访问这块内存,引用完毕也可以正常的回收。先来看一下如下的代码,有关引用计数的问题就会很清楚了!
5 | CCSprite * sprite = new CCSprite(); |
7 | CCLog("retain count:%d",sprite->retainCount()); |
10 | sprite->retain(); |
11 | CCLog("retain count:%d",sprite->retainCount()); |
14 | sprite->release(); |
15 | CCLog("retain count:%d",sprite->retainCount()); |
20 | sprite->autorelease(); |
21 | CCLog("retain count:%d",sprite->retainCount()); |
上边的代码有一处不是很好的理解,就是大家经常说的自动回收机制,也就是上边的autorelease方法,这个方法到底为我们做了什么,有什么作用,我们需要好好的搞清楚!首先需要澄清的一个概念就是帧,我们经常的说每秒多少多少帧,其实这个帧需要多少时间不是固定的,这个需要看每帧我们需要做多少事情,如果没一帧我们需要渲染很多的东西,那这一帧执行的时间当然就会很长的,游戏显得就会很卡,这个时候每秒的帧率就会下降的,所以不是时间决定的帧率,而是帧影响的时间!这个自动回收池就是在每帧结束的时候起作用的,在游戏的每一帧都会有一个大的循环,在一帧开始之前,系统建立了一个内存回收池,在这一帧的过程中,当我们调用了autorelease方法以后,我们的对象就会放到这个内存回收池中,当一帧结束的时候这个内存回收池就会释放掉,这个时候在内存回收池中的对象就会被release一下,也就是说引用计数就会减一,如果这个时候引用计数为0,就会删除对象了。如果引用计数不为0的话对象是不会被删除的,下一帧开始的时候系统又会创建一个内存回收池,这个时候在上一次添加的对象这个时候是不会重新添加到这个内存回收池中的,在这个内存回收池中的对象是你在这一帧中调用了autorelease函数的对象。好了,内存回收池我觉的我已经说清楚了。下面我们来看一下,通常我们的代码都是怎么写的,来看看这个自动回收机制怎么就做到自动回收了!
2 | CCSprite * sprite = CCSprite::create("HelloWorld.png"); |
3 | CCLog("retain count:%d",sprite->retainCount()); |
4 | this ->addChild(sprite); |
5 | CCLog("retain count:%d",sprite->retainCount()); |
首先我们需要分析一下上边的代码,调用了create工厂方法以后,内部的实现是先new一个CCSprite的对象,这个时候引用计数加1,然后调用autorelease方法,将这个对象放到了自动回收池中,因为这一帧还没有结束,当然引用计数就还是1,所以打印的结果就是1,当我们调用addChild的时候,传入这个CCSprite对象,这个时候在当前层接受了这个对象以后会把它的引用计数加一,表明当前层正在使用这块内存空间,所以现在的retain就是2了。当这一帧结束的时候自动回收池会将对象的引用计数-1,所以现在就只有CCLayer在引用这个对象了,当CCLayer析构的时候,它会调用这个对象的release方法,这个时候当然就会删除了这个CCSprite对象了。所以什么是自动回收机制呢,自动就是在这一帧结束的时候将对象开始new的时候加的那个引用计数减掉,而让引擎中持有对象引用的其他类去管理这个对象,当持有者析构的时候就删除引用,引擎中的类负责retain和release,这个也算是自动吧!下面我们通过俩个例子来理解一下这个内存管理机制。
1 | bool HelloWorld::init() |
4 | if ( !CCLayer::init() ) |
9 | CCMenuItemImage *pCloseItem = CCMenuItemImage::create( |
10 | "CloseNormal.png", |
11 | "CloseSelected.png", |
13 | menu_selector(HelloWorld::menuCloseCallback)); |
15 | CCMenu* pMenu = CCMenu::create(pCloseItem, NULL); |
17 | this ->addChild(pMenu, 1); |
19 | this ->m_sprite = CCSprite::create("HelloWorld.png"); |
20 | CCLog("%d", this ->m_sprite->retainCount()); |
25 | void HelloWorld::menuCloseCallback(CCObject* pSender) |
27 | CCLog("%d", this ->m_sprite->retainCount()); |
28 | this ->m_sprite->getPosition(); |
好了,运行程序我们会发现程序崩溃了,我们来分析一下。当我们创建了CCSprite对象的时候引用计数为1,并且该对象被放到了内存回收池中,在这一帧以后就会release对象一次,这个时候引用计数为0的对象当然就被释放了,我们按下按钮去调用这个对象,内存已经释放掉了,程序能不崩溃吗?下面我们来将代码改成如下的这样,看看效果。
1 | this ->m_sprite = CCSprite::create("HelloWorld.png"); |
2 | CCLog("%d", this ->m_sprite->retainCount()); |
3 | this ->m_sprite->retain(); |
4 | CCLog("%d", this ->m_sprite->retainCount()); |
这个时候运行起来程序,在按钮的函数调用的时候你会看到输出的引用计数都是1,因为我们已经手动的retain了一下这个对象,虽然自动回收池将它release了一下,但是它的引用计数任然为1。既然我们retain了,所以在层析构的时候记得要release啊。这样内存才不会泄露!还有一个就是CCArray的例子,下面来看一下。
1 | bool HelloWorld::init() |
10 | CC_BREAK_IF(! CCLayer::init()); |
11 | CCSprite* bomb1 = CCSprite::create("CloseNormal.png"); |
12 | CCSprite* bomb2 = CCSprite::create("CloseNormal.png"); |
13 | CCSprite* bomb3 = CCSprite::create("CloseNormal.png"); |
14 | CCSprite* bomb4 = CCSprite::create("CloseNormal.png"); |
15 | CCSprite* bomb5 = CCSprite::create("CloseNormal.png"); |
16 | CCSprite* bomb6 = CCSprite::create("CloseNormal.png"); |
25 | m_pBombsDisplayed = CCArray::create(bomb1,bomb2,bomb3,bomb4,bomb5,bomb6,NULL); |
27 | this ->scheduleUpdate(); |
35 | void HelloWorld::update(ccTime dt) |
40 | void HelloWorld::refreshData() |
42 | m_pBombsDisplayed->objectAtIndex(0)->setPosition(cpp(100,100)); |
其实这个原理和上边的例子是差不多的,我们创建了一个CCArray的对象,这个时候同样是没有添加到其他的层中的,所以在这一帧结束的时候就会将它的引用计数减1变成0,所以,再次使用的时候肯定就会出错了!在使用cocos2dx的内存管理的时候如果我们是通过工厂的方法创建的,并且add到了其他的层中的时候这个时候我们多数是不用担心的,但是如果你是通过new的方法创建的CCObject子类的对象,这个时候记住要在析构的时候release这个对象,如果在使用过程中你retain了对象,同样记住要release。本人总结的是,当我们调用函数传递CCObject子类对象的时候,在接受的时候我们都应该去retain一下,这样代表的是我要引用这块内存空间,什么时候你不再引用这块内存空间了,就release一下。其他的各种问题当你遇到的时候就想一下它的原理,就会明白的。最后的最后欢迎广大网友给我留言,我们共同探讨cocos2dx的问题!
以下是自己看代码的:
main函数中有这样一句:return Application::getInstance()->run();直接跳到这部分代码看下,while循环下有一句:director->mainLoop();
void DisplayLinkDirector::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (! _invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
可以看到,如果当前场景有效的话,调用了两句,这里看
PoolManager::getInstance()->getCurrentPool()->clear(),跳转到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
}
可以看到,在mainLoop函数中,也就是每一帧中,每次都对回收池中的对象进行了release操作,并且把当前的内存池清空,再看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;
}
}
对引用计数值减1,当为0的时候,直接从内存中删除。
下面拿sprite来看下:
Sprite* Sprite::create(const std::string& filename)
{
Sprite *sprite = new Sprite(); //引用计数为1
if (sprite && sprite->initWithFile(filename))
{
sprite->autorelease();//并且交给回收池管理
return sprite;
}
CC_SAFE_DELETE(sprite);
return nullptr;
}
当进行addchild操作时,有一句this->insertChild(child, zOrder);,跟进去:
void Node::insertChild(Node* child, int z)
{
_reorderChildDirty = true;
_children.pushBack(child);
child->_setLocalZOrder(z);
}
再看pushBack,
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();
}
这里又retain了一次,引用计数为2了。在上面一帧结束的时候release就变成了1.这时只有他的parent对他拥有引用计数1.当parent析构时,
void Node::removeFromParentAndCleanup(bool cleanup)
{
if (_parent != nullptr)
{
_parent->removeChild(this,cleanup);
}
}
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
// explicit nil handling
if (_children.empty())
{
return;
}
ssize_t index = _children.getIndex(child);
if( index != CC_INVALID_INDEX )
this->detachChild( child, index, cleanup );
}
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
// IMPORTANT:
// -1st do onExit
// -2nd cleanup
if (_running)
{
child->onExitTransitionDidStart();
child->onExit();
}
#if CC_USE_PHYSICS
if (child->_physicsBody != nullptr)
{
child->_physicsBody->removeFromWorld();
}
#endif
// If you don't do cleanup, the child's actions will not get removed and the
// its scheduledSelectors_ dict will not get released!
if (doCleanup)
{
child->cleanup();
}
// set parent nil at the end
child->setParent(nullptr);
_children.erase(childIndex);
}
iterator erase(ssize_t index)
{
CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
auto it = std::next( begin(), index );
(*it)->release();
return _data.erase(it);
}
进行了release操作,这时引用对数为0,最后设置他的父节点为空,并且从父节点子节点数组中删除。