Cocos2dx内存管理之引用计数分析

前言: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);      // 将当前对象保存到向量中
}

2、将精灵添加到场景中(添加到另一个节点上):

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;
}
这里可以看到有个变量:Vector _children;        ///< array of children nodes

Vector类这里不多说,具体请百度吧!跟进_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_children; ,这个_children中就保存的时当前节点下的所有子节点(这里有个递归的概念);当Node对象被析构的时候,_children也会调用自身的析构函数,那么看看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();
}

好了,整个流程就结束了。这个引用计数的逻辑很完美吧!


你可能感兴趣的:(Cocos2dx,C++)