简述
cocos2dx使用引用计数法(计数变量为基类Ref的成员变量_referenceCount)自动管理内存,自动回收池AutoReleasePool由PoolManager进行管理,每当使用create函数创建对象时,对象的_referenceCount为1并加入到自动回收池中,之后引用此对象/取消引用会同步加/减计数变量。当每帧事件处理结束时,自动回收池将池中的对象的计数减1并清空回收池,此时若某对象计数为0,则说明这个对象没有被任何对象引用,则会释放这个对象。若这帧之后某个被引用的对象被取消引用后计数为0,则会立刻释放这个对象。
源码分析
创建节点时
以Node为例:
Node * Node::create(){
//std::nothrow代表当内存不足以new对象时不抛出异常而是使返回值变为nullptr
Node * ret = new (std::nothrow) Node();
//如果创建对象成功并且初始化成功
if (ret && ret->init()){
//将节点放入自动回收池
ret->autorelease();
} else {
//如果创建失败,删除节点
CC_SAFE_DELETE(ret);
}
return ret;
}
当调用Node::create()时,在new Node()的过程中首先调用基类Ref的构造函数,初始化计数变量为1:
//_referenceCount是内存管理中引用计数法的计数变量,通过此变量记录该对象被引用的个数
Ref::Ref() : _referenceCount(1) //...
{
//...
}
,随后调用autorelease函数将节点自身放入自动回收池:
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
引用/取消引用节点时
当执行addChildren、addAction、setScheduler等函数时,最终都会调用被引用对象的retain函数,以增加计数:
//addChild->addChildHelper->insertChild
void Node::insertChild(Node* child, int z)
{
//...
_children.pushBack(child);
//...
}
//这里的Vector是cocos根据STL的vector写的包装类,在加入对象\删除对象时会自动给对象计数
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
//调用retain函数
object->retain();
}
void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
++_referenceCount;
}
当执行removeChildren等函数时,最终都会调用被引用对象的release函数,以减少计数,同时如果计数为0,则会立即释放节点:
//removeChild->detachChild
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
//...
_children.erase(childIndex);
}
iterator erase(ssize_t index)
{
CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
auto it = std::next( begin(), index );
//调用release函数
(*it)->release();
return _data.erase(it);
}
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
//...
delete this;
}
}
自动回收
在每帧调用的Director::mainLoop函数中,在处理事件之后调用了PoolManager::getInstance()->getCurrentPool()->clear()来清理自动回收池的对象:
void Director::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (_restartDirectorInNextLoop)
{
_restartDirectorInNextLoop = false;
restartDirector();
}
else if (! _invalid)
{
//每帧的事件在这里处理
drawScene();
//处理结束之后清理自动回收池
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
在clear函数中,将回收池中的所有对象依次调用release函数并清空回收池:
void AutoreleasePool::clear()
{
//...
std::vector<Ref*> releasings;
//将两个数组的数据空间指针交换,即把_managedObjectArray数组置空
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
//这里减的时刚创建对象时的那1个计数
obj->release();
}
//...
}
也就是说,只有当前帧内创建的对象才会被放入自动回收池中(并在该帧清理结束时移出自动回收池),如果这个对象在创建的这帧内没有被任何引用,才会被自动回收池释放,这样保证剩下的对象都有其他对象引用。在之后的帧中,如果对象被取消引用导致计数为0,则会被取消引用时调用的release函数立刻释放(与自动回收池无关)。
内存泄漏检查
cocos2dx中存在一个宏定义CC_REF_LEAK_DETECTION,开启后可以使用Ref::printLeaks()函数打印当前时刻所有存在的以Ref为基类的对象类型与引用计数。
Ref::Ref()//...
{
//如果开启了cocos对象内存泄漏检查
#if CC_REF_LEAK_DETECTION
//记录这个对象
trackRef(this);
#endif
}
static void trackRef(Ref* ref)
{
//在此函数内给变量加锁,防止多线程访问更改变量
std::lock_guard<std::mutex> refLockGuard(__refMutex);
CCASSERT(ref, "Invalid parameter, ref should not be null!");
//将这个对象记录在数组中,这个数组保存了所有存在的对象
__refAllocationList.push_back(ref);
}
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
//...
#if CC_REF_LEAK_DETECTION
//取消记录对象
untrackRef(this);
#endif
delete this;
}
}
Ref::~Ref()
{
//...
#if CC_REF_LEAK_DETECTION
if (_referenceCount != 0)
//疑问:为什么不统一在析构函数中调用
untrackRef(this);
#endif
}
static void untrackRef(Ref* ref)
{
std::lock_guard<std::mutex> refLockGuard(__refMutex);
//从数组中找到指向需要移除的对象的迭代器
auto iter = std::find(__refAllocationList.begin(), __refAllocationList.end(), ref);
//如果对象不存在
if (iter == __refAllocationList.end())
{
log("[memory] CORRUPTION: Attempting to free (%s) with invalid ref tracking record.\n", typeid(*ref).name());
return;
}
//移除对象
__refAllocationList.erase(iter);
}
void Ref::printLeaks()
{
std::lock_guard<std::mutex> refLockGuard(__refMutex);
if (__refAllocationList.empty())
{
log("[memory] All Ref objects successfully cleaned up (no leaks detected).\n");
}
else
{
log("[memory] WARNING: %d Ref objects still active in memory.\n", (int)__refAllocationList.size());
for (const auto& ref : __refAllocationList)
{
CC_ASSERT(ref);
const char* type = typeid(*ref).name();
log("[memory] LEAK: Ref object '%s' still active with reference count %d.\n", (type ? type : ""), ref->getReferenceCount());
}
}
}