Why?
内存管理一直是个很重要的事情,需要考虑分配和回收的时机,千万不能出现内存泄漏,因为手机上内存本来就有限。内存管理的核心就是动态分配的内存及时回收。cocos2dx是从cocos2d-iphone发展过来的,使用的引用计数的机制进行管理。了解了cocos中的内存管理机制,就可以明确的了解在什么时候retian在什么时候release。我尽量只说关键的地方,不贴大量的代码。
引用计数
刚开始了解cocos的时候,看了一些书,书上说cocos中是使用的引用计数的机制进行管理内存。当时想了下,很好理解啊,每一帧去检查一下每个对象的引用计数,如果是0则释放掉。最近深入看了一下源码,发现不是每帧去检查每个对象,每个对象只会被检查一次,后面就在恰当的时机进行释放。
自动释放池
AutoreleasePool里面就是封装了一个vector,并对vector中的对象进行管理。
自动释放池管理
PoolManager,此类是对自动释放池的管理,看到有这个管理类,就知道自动释放池可能不止一个,是的,自动释放池可以创建多个,都由PoolManager这个类管理。PoolManager使用栈的操作来管理自动释放池。
Ref类
此类中有个变量保存引用计数,继承此类,就可以很方便的加入自动释放池中管理。引用计数初始的值为1
Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1
一个Node被管理的过程
1:创建一个node
Node* node = Node::create();
此时的node的引用计数是1
调用了Node的create工厂方法进行创建,查看create方法的实现
Node * Node::create()
{
Node * ret = new (std::nothrow) Node();
if (ret && ret->init())
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
node调用了autorelease方法,此方法为Ref类的方法
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
其实是把当前的node放到了自动释放池中进行管理。PoolManager调用getInstance的方法获取实例,查看里面的实现
PoolManager* PoolManager::getInstance()
{
if (s_singleInstance == nullptr)
{
s_singleInstance = new (std::nothrow) PoolManager();
// Add the first auto release pool
new AutoreleasePool("cocos2d autorelease pool");
}
return s_singleInstance;
}
可以看到自动释放池也是在此时创建的。以前引擎版本的自动释放池是在引擎初始化的时候创建的。
PoolManager类的getCurrentPool方法返回的是当前的自动释放池
AutoreleasePool* PoolManager::getCurrentPool() const
{
return _releasePoolStack.back();
}
_releasePoolStack是一个vector,但是现在当成栈来用。
2:在mainLoop中进行处理
mainLoop是上篇文章说的主循环的操作,每一帧都会调用
void Director::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (_restartDirectorInNextLoop)
{
_restartDirectorInNextLoop = false;
restartDirector();
}
else if (! _invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
此处调用了PoolManager::getInstance()->getCurrentPool()->clear()查看clear中的代码
std::vector releasings;
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
obj->release();
}
_managedObjectArray为自动释放池中的对象容器;releasings是个空的容器。
经过releasings.swap(_managedObjectArray)这个操作之后_managedObjectArray就是空的了,就是说加入了自动释放池中的对象只会被管理一次,后面不会再被管理了。
对releasings中的对象(其实就是刚才内存释放池中的对象)进行遍历,进行release操作,此为Ref类中的方法
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be 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
#if CC_ENABLE_SCRIPT_BINDING
ScriptEngineProtocol* pEngine = ScriptEngineManager::getInstance()->getScriptEngine();
if (pEngine != nullptr && pEngine->getScriptType() == kScriptTypeJavascript)
{
pEngine->removeObjectProxy(this);
}
#endif // CC_ENABLE_SCRIPT_BINDING
#if CC_REF_LEAK_DETECTION
untrackRef(this);
#endif
delete this;
}
}
大家可以看到,把引用计数减1,如果此时引用计数为0,则释放此Ref对象。
刚才创建的node,此时就会被释放掉了。如果使用了成员变量或者全局变量指向了刚才的node,那么刚才的node就是个野指针了。
3:添加到场景中的Node的引用计数情况
创建了的node添加到场景(其实也是个Node)中,此时引用计数就会+1,变成2
Node* node = Node::create(); // 此时node的引用计数为1
Layer* layer = Layer::create();
layer->addChild(node); // 此时node的引用计数为2
为什么addChild了以后引用计数就变成2了呢,来看看代码
void Node::addChild(Node* child, int localZOrder, const std::string &name)
调用addChildHelper(child, localZOrder, INVALID_TAG, name, false)
调用Node::insertChild(Node* child, int z)
调用_children.pushBack(child)
调用_data.push_back( object );
object->retain();
调用++_referenceCount;
原来Node(Layer继承自Node)内部有个CCVector,每次添加Ref到CCVector都会retain一下,会增加一次引用计数。
相应的在removechild的时候会调用release,此时引用计数减一,如果此时引用计数为0了,则立刻释放,就不用在自动释放池中释放了。所以自动释放池只要管理一次就可以了。
4:如何让Ref不被释放
其实很简单,retain一下就可以了。
调用addChild方法会增加一次引用计数。有些Ref对象是不用添加到场景中的,比如CCArray,只是用来对场景中的元素进行管理,那么在创建一个CCArray对象以后,主动retain一下,就不会被自动释放池清除掉。但是在场景结束时,要relase掉,不然就会出现内存泄漏。
自动释放池的嵌套
如果创建了大量的Ref变量,那么在mainLoop的时候就有可能出现释放时间过长,导致下一次绘图间隔太久,导致界面上卡顿。如果有此种情况,可以考虑自己创建自动释放池来存放Ref变量,用完就释放掉。
此时PoolManager的管理功能就派上用场了。下面来看看这个过程。
使用场景:创建了大量的String(cocos中封装的字符类,非标准库中的类)对象对字符串进行操作,由于字符对象是不用加载到场景中的,所以在使用完成以后在本帧结束后就会被释放掉。
AutoreleasePool* pool = new AutoreleasePool("My release pool"); // 创建我们自己的auto release pool
String* string1 = String::createWithFormate("nihao%d", 1);
...
...
String* string1000 = String::createWithFormate("nihao%d", 1000);
delete pool; // 释放我们自己创建auto release pool
分析一下上述流程:
1:创建了一个我们自己的自动释放池名字叫:My release pool。查看AutoreleasePool的构造函数
AutoreleasePool::AutoreleasePool(const std::string &name)
: _name(name)
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);
}
PoolManager会将我们刚刚创建的自动释放池加入到内部的栈中。此时通过getCurrentPool获取的就是刚才创建的自动释放池。
2:创建字符串变量指针,createWithFormate里面调用了autorelease函数,此时这些变量的指针都会添加到我们刚才创建的自动释放池中。
3:删除自动释放池。查看AutoreleasePool的析构函数。
AutoreleasePool::~AutoreleasePool()
{
CCLOGINFO("deallocing AutoreleasePool: %p", this);
clear();
PoolManager::getInstance()->pop();
}
此时会先调用clear函数释放池中的指针。然后PoolManager会将当前的自动释放池从内部的栈中移除。此时PoolManager调用getCurrentPool返回的就是默认的自动释放池。
总结:
1:自动释放池对每个Ref对象指针管理一次,后面就在恰当的时机进行释放。
2:CCVetctor在添加Ref对象指针的时候会retain一次,在移除的时候会release一次。
3:自动释放池可以嵌套使用。