COCOS學習筆記--Cocod2dx內存管理(三)-Coco2d-x內存運行原理

通過上兩篇博客,我們對Cocos引用計數和Ref類、PoolManager類以及AutoreleasePool類已有所瞭解,那麼接下來就通過舉栗子來進一步看看Coco2d-x內存運行原理是怎樣的。

 //先建一個node
Node * node = Node::create();
//創建完之後打印node的引用計數
schedule([node](float f){
//獲得node的引用計數
int count = node->getReferenceCount();
//打印node的引用計數
log("node's ReferenceCount = %d",count);
},"test");

打印結果如下:


可以看到引用計數打印出來是一大串的整數,其實這時node已經被釋放掉了是不存在的,引用計數爲0,所以系統隨便打印了一堆整數來表示。

我們先看一下Node類的源碼,其create()方法如下:

Node * Node::create()
{
    Node * ret = new (std::nothrow) Node();
    if (ret && ret->init())
    {
        ret->autorelease();
    }
    else
    {
        CC_SAFE_DELETE(ret);
    }
    return ret;
}

看到這句Node * ret = new (std::nothrow) Node();首先new了一個node,我們知道Node是繼承自Ref的,通過new創建對象就會調用其構造函數,而之前我們在Ref類的講解中知道:調用Ref構造函數中會執行這句_referenceCount(1)對其引用計數+1,所以node對象起始的引用計數爲1。

我們還可以看到這句:ret->autorelease(),只要通過create創建的node都會被添加到自動釋放池中,我們在看下autorelease()源碼:

Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}

既然node對象起始的引用計數爲1,爲什麼在打印時它的引用計數就爲0了呢?node對象是在什麼時候被釋放的呢?

先別急,我們先讓node不被釋放,如何使node不被釋放,有兩種方法:


方法1通過retain()方法增加node引用計數

//增加node的引用計數
node->retain();

node最開始創建後引用計數爲1,調用retain()方法使其引用計數再+1,此時node的引用計數爲2,但剛剛也看到了,引用計數開始後不知什麼時候減了1,所以打印出來應該是1。運行一下:


可以看出,通過調用node的retain()方法可以使其引用計數+1。


方法2:通過addChild()方法將node添加到父節點上

this->addChild(node);

運行效果如下:


可以看到,通過addChild()方法也可以使node的引用計數+1,這是爲什麼呢

我們再看一下Node類的源碼:

void Node::addChild(Node *child)
{
    CCASSERT( child != nullptr, "Argument must be non-nil");
    this->addChild(child, child->_localZOrder, child->_name);
}

可以看到node的addChild()方法調用了其重載的addChild()方法,那我們就接着看這重載的addChild()方法都做了什麼:

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);
}

在重載addChild()方法中我們看到調用了這句:

addChildHelper(child, localZOrder, tag, "", true);

這句是幹什麼的呢我們接着看:

void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
    ......
    
    this->insertChild(child, localZOrder);
    
    ......
}

在Node::addChildHelper()方法中調用了這句:

this->insertChild(child, localZOrder)

那我們就繼續看insertChild()方法:
void Node::insertChild(Node* child, int z)
{
    _transformUpdated = true;
    _reorderChildDirty = true;
    _children.pushBack(child);
    child->_localZOrder = z;
}

我們看到了執行了這句代碼:

_children.pushBack(child);

這句很關鍵,我們到CCvector.h文件中看它的具體實現:

  void pushBack(T object)
    {
        CCASSERT(object != nullptr, "The object should not be nullptr");
        _data.push_back( object );
        object->retain();
}

可以看到,通過pushBack方法將傳來的child對象添加到了_data這個數據結構中,然後對child執行了其retain()方法,這下大家都明白了吧!爲什麼調用addChild()方法會使child的引用計數+1,因爲它最後還是調用了retain()方法!


好了,以上兩種增加引用計數的方法介紹完了。回過頭來,剛剛還有一個問題一直沒有解決:就是我們創建的node明明在創建後引用計數爲1,如果不人爲通過以上2種方法增加其引用計數,爲什麼程序一啓動引用計數就變成0了呢?這個node是在什麼時候被釋放的呢?

接下來我就爲大家解答一下:

之前在我寫渲染流程的博客(http://blog.csdn.net/gzy252050968/article/details/50414407)中提到過,引擎的入口函數是CCApplication類的run()方法,

 int Application::run()  
{     
     ......         
     director->mainLoop();//進入引擎的主循環  
     ......      
     return 0;  
 }

在run()方法中進入了遊戲的主循環mainLoop(),我們設置的幀率就是mainLoop()方法每秒執行的次數,一般默認是每秒執行60次,我們遊戲的渲染、內存管理等等全都是在這個mainLoop()方法裏不斷執行的。

我們再看一下這個主循環mainLoop():

void DisplayLinkDirector::mainLoop()  
{  
     if (_purgeDirectorInNextLoop)//進入下一個主循環,也就是結束這次的主循環,就淨化,也就是一些後期處理   
     {  
         _purgeDirectorInNextLoop = false;  
         purgeDirector();  
     }  
    else if (_restartDirectorInNextLoop)  
    {  
        _restartDirectorInNextLoop = false;  
        restartDirector();  
     }  
     else if (! _invalid)  
     {  
        drawScene();//繪製屏幕  
         PoolManager::getInstance()->getCurrentPool()->clear();//釋放一些沒有用的對象,主要保件內存的合理管理   
     }  
 } 

我們可以看到這句代碼:

PoolManager::getInstance()->getCurrentPool()->clear();

這句就是在每一幀結束時釋放沒有用到的對象,具體過程是先通過PoolManager::getInstance()方法獲得PoolManager的單例對象,然後再通過getCurrentPool()方法得到當前的自動釋放池對象,最後執行AutoreleasePool的clear()方法,clear()方法我在上一篇博客裏寫過,這裏再去CCAutoreleasePool.cpp中看一下:

void AutoreleasePool::clear()
{
    #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
        _isClearing = true;//設置爲執行了清空操作
    #endif
    std::vector<Ref*> releasings;
    releasings.swap(_managedObjectArray);
    //遍歷自動釋放池managedObjectArray裏存放的所有的Ref
    for (const auto &obj : releasings)
    {
        //調用obj的release(),對obj的引用計數-1(如果對象引用計數爲0則刪除)
        obj->release();
    }
    #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
        _isClearing = false;//設置爲未執行清空操作
    #endif
}

clear()方法就是AutoreleasePool對象把自己維護的隊列managedObjectArray裏面每一個obj都執行release()。

release()方法我的上上篇博客裏也介紹過,這裏再看一下加深記憶:

void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
    //對其引用計數值進行-1
    --_referenceCount;
    //引用計數爲0,刪除對象
    if (_referenceCount == 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
        //從保存Ref*的list中刪除
        #if CC_REF_LEAK_DETECTION
            untrackRef(this);
        #endif
        //刪除該對象
        delete this;
    }
}

看到這句了沒?

--_referenceCount;

看到這句了沒?

delete this;

release()方法分兩部分,先對引用計數-1,然後判斷引用計數是否爲0,若爲0則刪除對象。

如果我們只通過create()去創建一個對象,而不去調用其retain()方法,那麼這個對象就會在下一幀被釋放掉。

這回你理解爲什麼我們一開始創建的node直接就被釋放掉了吧。


好了,重點來了,我們看到人爲通過以上2種方法讓引用計數+1後,打印出來的引用計數一直是1,按理來說,它們是通過create()創建出來的,在每一幀結束後AutoreleasePool::clear()方法中也會調用其release()方法,這一幀是1,下一幀就該減1變成 0了啊,爲什麼沒有變成0被釋放掉呢?

爲什麼啊?爲什麼啊?

其實是這樣的,我們每次執行AutoreleasePool::clear()後,都會對AutoreleasePool維護的隊列_managedObjectArray執行一次clear(),也就是說在下一幀的時候,自動釋放池裏已經不存在這些node了,所以在AutoreleasePool::clear()中便不會執行之前這些node的release()方法,這些node在下一幀並不會被釋放,這就是爲什麼node的引用計數打印出來一直是1,說白了就是在AutoreleasePool::clear()中每個node只會執行一次release()方法。


那麼接下來你可能會想,既然在自動釋放池中只執行一次node的release()方法,那麼如何去刪除node呢?

很簡單:之前介紹了2種增加node引用計數的方法,1是retain()方法2是addChild()方法,那麼要刪除node的方法與以上2個一一對應:

方法一.通過release()方法減少node引用計數;

方法二.通過removeFromParent()方法將node從父節點上移除。

removeFromParent()方法其實也是在其方法中執行release()方法進行了刪除操作,但該方法不是隻執行了單純的刪除操作,它還從渲染樹中將node移除。Cocos2d-x是通過渲染樹進行渲染的,對cocos引擎渲染流程不是很瞭解的可以看看我之前的博客,cocos是將所有節點添加到渲染樹上進行渲染的。




最後總結一下:

1.create出的node對象起始引用計數爲1;

2.增加node的引用計數方法有2種:調用retain()方法使引用計數直接+1或通過addChild()方法將node添加到父節點上使其引用計數+1;

3.減少node的引用計數方法有2種:調用release()方法使引用計數直接-1或通過removeFromParent()方法將node從父節點上移除;

4.主循環mainLoop()方法中在每幀都會執行AutoreleasePool::->clear()釋放自動釋放池中沒有用到的對象;

5.如果只通過create()去創建一個對象node,而不去調用其retain()方法,那麼這個node就會在下一幀被釋放掉。

 

 

 


好了,關於Cocos引擎內存管理的所有內容就都OK了,花了一個週末連學習帶總結,累死寶寶了<@_@> 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章