前言:Cocos2dx游戏引擎是使用C++语言写的,为了使程序员从繁琐的内存管理工作中解放出来,cocos模仿IOS,采用了一套引用计数机制来管理内存的分配和销毁。那么它是怎么管理这么庞大的一个引擎,而没有导致内存泄漏的,很好奇就研究了一下源码,跟进了一遍主循环,发现确实是很巧妙!
一、引用计数管理类:Ref
引擎中定义了一个基类Ref,专门用于管理引用计数。其中最主要的核心就是维护了一个无符号整形变量unsignedint _referenceCount; 这个变量在对象刚刚创建的时候会被初始化为1,如果值为0的时候就代表需要被销毁了。其中主要的几个方法是:
void retain(); // 引用计数加1
void release(); // 引用计数减1
Ref* autorelease(); // 将对象添加到自动释放池中,引用计数不会发生变化
二、怎么创建对象?
Cocos2dx引擎中的绝大部分类都继承了Ref类,即凡是希望将内存管理工作交由引擎管理的对象都继承自Ref类。最主要的就是Node类(节点),Sprite(精灵)继承自Node,这里就以创建一个Sprite对象为例,分析一遍内存管理的流程。
1、创建一个普通的精灵节点:
auto logo = Sprite::create("Images/Home/logo.png"); // 创建一个精灵节点
跟进create()方法,在源码中可以看到大量的如下写法:
Sprite* Sprite::create(const std::string& filename)
{
Sprite *sprite = new (std::nothrow) Sprite(); // 构造一个新对象,此时引用计数_referenceCount值为1
if (sprite && sprite->initWithFile(filename))
{
sprite->autorelease(); // 这句很关键,这里就是将对象加入到自动释放池中,交由引擎来管理内存
return sprite;
}
CC_SAFE_DELETE(sprite);
return nullptr;
}
跟进autorelease()方法:
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this); // 继续往下看
return this;
}
进入addObject(Ref* object)方法:
std::vector _managedObjectArray;
void AutoreleasePool::addObject(Ref* object)
{
_managedObjectArray.push_back(object); // 将当前对象保存到向量中
}
logo->setPosition(200, 500);
addChild(logo); // 添加到当前Layer(也是一个节点)上
跟进addChild()方法:
void Node::addChild(Node *child)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
this->addChild(child, child->_localZOrder, child->_name); // 继续往下
}
void Node::addChild(Node *child, int localZOrder, int tag)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
CCASSERT( child->_parent == nullptr, "child already added. It can't be added again");
addChildHelper(child, localZOrder, tag, "", true); // 继续往下
}
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
if (_children.empty())
{
this->childrenAlloc();
}
this->insertChild(child, localZOrder); // 这句很关键,继续往下
if (setTag)
child->setTag(tag);
else
child->setName(name);
child->setParent(this);
child->setOrderOfArrival(s_globalOrderOfArrival++);
if( _running )
{
child->onEnter();
// prevent onEnterTransitionDidFinish to be called twice when a node is added in onEnter
if (_isTransitionFinished)
{
child->onEnterTransitionDidFinish();
}
}
if (_cascadeColorEnabled)
{
updateCascadeColor();
}
if (_cascadeOpacityEnabled)
{
updateCascadeOpacity();
}
}
跟进this->insertChild(child, localZOrder);这句:
// helper used by reorderChild & add
void Node::insertChild(Node* child, int z)
{
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child); // 这句很关键,将child保存到向量中
child->_localZOrder = z;
}
这里可以看到有个变量:VectorVector类这里不多说,具体请百度吧!跟进_children.pushBack(child);这句看看到底做了什么操作:
/** Adds a new element at the end of the Vector. */
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object ); // 将对象保存到向量中
object->retain(); // 这里可以看到引用计数加1了,此时引用计数_referenceCount值为2
}
三、怎么销毁对象(释放内存)?
这里我们倒过来分析,查看Ref类的release()方法:
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount; // 引用计数减1
#if CC_ENABLE_SCRIPT_BINDING && CC_ENABLE_GC_FOR_NATIVE_OBJECTS
if (_scriptOwned && _rooted && _referenceCount==/*_referenceCountAtRootTime*/ 1)
{
auto scriptMgr = ScriptEngineManager::getInstance()->getScriptEngine();
if (scriptMgr && scriptMgr->getScriptType() == kScriptTypeJavascript)
{
scriptMgr->unrootObject(this);
}
}
#endif // CC_ENABLE_SCRIPT_BINDING
if (_referenceCount == 0) // 如果引用计数等于0,则销毁对象
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
auto poolManager = PoolManager::getInstance();
if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
{
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
#if CC_REF_LEAK_DETECTION
untrackRef(this);
#endif
delete this; // 销毁当前对象
}
}
这里可以看到,只有当引用计数_referenceCount的值为0时,才会销毁对象。上面我们分析到,_referenceCount的值为2了,那么这两次减法操作(Release())在哪里执行的呢?
1、第一次release:
查看游戏主循环mainLoop():
void DisplayLinkDirector::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(); 这句:
void AutoreleasePool::clear()
{
std::vector releasings;
// swap()方法用得很高级,_managedObjectArray赋值的同时也清空了自身,比clear()效果好
// _managedObjectArray中保存的就是我们上文中创建的精灵对象(logo)
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
obj->release(); //此时引用计数_referenceCount值为1
}
}
这时
_managedObjectArray已经被清空,在下一帧的时候就不会再调用release()了。
2、第二次release:
第二次调用release()方法就比较隐蔽了,在场景切换后,当前场景被销毁的时候调用。
跟进游戏主循环:mainLoop() --> drawScene() --> setNextScene() :
void Director::setNextScene()
{
bool runningIsTransition = dynamic_cast(_runningScene) != nullptr;
bool newIsTransition = dynamic_cast(_nextScene) != nullptr;
// If it is not a transition, call onExit/cleanup
if (! newIsTransition)
{
if (_runningScene)
{
_runningScene->onExitTransitionDidStart();
_runningScene->onExit();
}
// issue #709. the root node (scene) should receive the cleanup message too
// otherwise it might be leaked.
if (_sendCleanupToScene && _runningScene)
{
_runningScene->cleanup();
}
}
if (_runningScene)
{
_runningScene->release(); // 释放当前场景
}
_runningScene = _nextScene;
_nextScene->retain();
_nextScene = nullptr;
if ((! runningIsTransition) && _runningScene)
{
_runningScene->onEnter();
_runningScene->onEnterTransitionDidFinish();
}
}
看到这里可能就会有疑问了,这里貌似只有释放场景的操作,没有看到释放场景中的节点(子节点)的操作啊? 所以说这里有点隐蔽了。
_runningScene->release();释放当前场景(也是一个Node对象)后,就会调用它的析构函数。而Node对象中保持了一个Vector对象Vector
/** Destructor. */
~Vector()
{
CCLOGINFO("In the destructor of Vector.");
clear(); // 继续往下
}
void clear()
{
for( auto it = std::begin(_data); it != std::end(_data); ++it ) {
(*it)->release(); // 这里就是最后一次release了,此时引用计数_referenceCount值为0,然后就会销毁当前节点对象了
}
_data.clear();
}