Cocos2d-x 3.2 的內存管理詳解


目標讀者:瞭解 Cocos2d-x 中的節點以及節點樹,瞭解引用計數,瞭解遊戲主循環等概念。html


本文首先介紹 Cocos2d-x 3.2 中內存管理的做用,以及各個做用的應用。藉由通俗易懂的解釋來了解內存管理的過程。其次經過源碼解析介紹其內部的實現原理。加深理解,從而在有須要的時候繞開引擎創建本身的內存管理機制。node

1、Cocos2d-x 3.2 內存管理的兩個方面


1) 經過加入 autorelease 來自動釋放那些建立後未使用的對象。
2) 經過節點管理來保證對象在棄用後及時地刪除。git

一、及時釋放棄用的對象

使用條件:該對象是Node的子類對象
使用方法:addChild、removeChildgithub

內存管理過程:數組

addChild //添加對象後,對象能夠被使用
removeChild //刪除對象後,對象被馬上刪除(經過 delete)

二、及時釋放未使用的對象

簡述新建立的對象若是一幀內不使用,就會被自動釋放。(所謂一幀,便是一個gameloop。)
使用條件:對象經過CREAT_FUNC()宏建立或者對象使用autorelease加入了自動釋放池。
使用方法:自動實現函數

內存管理過程:oop

  • 對象不使用的狀況
對象建立 引用+1
對象自動釋放 引用-1
  • 對象使用的狀況
對象建立 引用+1
對象使用 引用+1 // 經過 addChild 使用對象
對象自動釋放 引用-1

引用的初始值爲0,若是一幀結束後對象的引用值仍是0,那就就會被 delete。性能



2、內存管理的實現原理

涉及內存管理的文件不少,僅展現直接相關的部分代碼。this

一、第一部分

Ref 類: 進行引用計數、提供加入自動釋放池的接口。lua

AutoreleasePool 類: 管理一個 vector 數組來存放加入自動釋放池的對象。提供對釋放池的清空操做。

PoolManager 類: 管理一個 vector 數組來存放自動釋放池。默認狀況下引擎只建立一個自動釋放池,所以這個類是提供給開發者使用的,例如出於性能考慮添加本身的自動釋放池。

DisplayLinkDirector 類: 這是一個導演類,提供遊戲的主循環,實現每一幀的資源釋放。這個類的名字看起來有點怪,可是不用管它。由於這個類繼承了 Director 類,也是惟一一個繼承了 Director 的類,也就是說徹底能夠合併爲一個類,引擎開發者在源碼中有部分說明。

1.1 Ref

// 引用計數變量
unsigned int _referenceCount;

// 對象被構造後,引用計數值爲 1
Ref::Ref()
: _referenceCount(1) //當Ref對象被建立時,引用計數的值爲 1
{
#if CC_ENABLE_SCRIPT_BINDING
    static unsigned int uObjectCount = 0;
    _luaID = 0;
    _ID = ++uObjectCount;
    _scriptObject = nullptr;
#endif
    
#if CC_USE_MEM_LEAK_DETECTION
    trackRef(this);
#endif
}

// 引用+1
void Ref::retain()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    ++_referenceCount;
}

// 引用-1 。若是引用爲0則釋放對象
void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    --_referenceCount;

    if (_referenceCount == 0)
    {

#if CC_USE_MEM_LEAK_DETECTION
        untrackRef(this);
#endif
        delete this; // 注意這裏 把對象 delete 了
    }
}

// 提供加入自動釋放池的接口。對象調用此函數便可加入自動釋放池的管理。
Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}

//獲取引用計數值
unsigned int Ref::getReferenceCount() const
{
    return _referenceCount;
}

1.2 AutoreleasePool

// 存放釋放池對象的數組
std::vector<Ref*> _managedObjectArray;

// 往釋放池添加對象
void AutoreleasePool::addObject(Ref* object)
{
    _managedObjectArray.push_back(object);
}

// 清空釋放池,將其中的全部對象都 delete
void AutoreleasePool::clear()
{
    // 釋放全部對象
    for (const auto &obj : _managedObjectArray)
    {
        obj->release();
    }
    // 清空vector數組
    _managedObjectArray.clear();
}

// 查看某個對象是否在釋放池中
bool AutoreleasePool::contains(Ref* object) const
{
    for (const auto& obj : _managedObjectArray)
    {
        if (obj == object)
            return true;
    }
    return false;
}

1.3 PoolManager

// 釋放池管理器單例對象
static PoolManager* s_singleInstance;

// 釋放池數組
std::vector<AutoreleasePool*> _releasePoolStack;

// 獲取 釋放池管理器的單例
PoolManager* PoolManager::getInstance()
{
    if (s_singleInstance == nullptr)
    {
        // 新建一個管理器對象
        s_singleInstance = new PoolManager(); 
        
        // 添加一個自動釋放池
        new AutoreleasePool("cocos2d autorelease pool");// 內部使用了釋放池管理器的push,這裏的調用很微妙,讀者能夠動手看一看
    }
    return s_singleInstance;
}

// 獲取當前的釋放池
AutoreleasePool* PoolManager::getCurrentPool() const
{
    return _releasePoolStack.back();
}

// 查看對象是否在某個釋放池內
bool PoolManager::isObjectInPools(Ref* obj) const
{
    for (const auto& pool : _releasePoolStack)
    {
        if (pool->contains(obj))
            return true;
    }
    return false;
}

// 添加釋放池對象
void PoolManager::push(AutoreleasePool *pool)
{
    _releasePoolStack.push_back(pool);
}

// 釋放池對象出棧
void PoolManager::pop()
{
    CC_ASSERT(!_releasePoolStack.empty());
    _releasePoolStack.pop_back();
}

1.4 DisplayLinkDirector

void DisplayLinkDirector::mainLoop()
{
    //第一次當導演
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();//進行清理工做
    }
    else if (! _invalid)
    {
        // 繪製場景,遊戲主要工做都在這裏完成
        drawScene();
     
        // 清空資源池
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

根據目前的分析,咱們先來捋一捋,待會兒再進一步深刻。內存管理的過程是怎麼樣的呢?首先,建立了一個 Node 對象A,Node 繼承Ref,所以 Ref 的引用計數爲1;而後,A經過 autorelease 將本身放入自動釋放池;drawScene() 完成後,一幀結束,Director 經過釋放池將池中的對象 clear(),即對 Node 對象A進行 release() 操做。A的引用計數變爲0,執行 delete 釋放A對象。

接下來咱們繼續介紹另外幾個與內存管理有關的類。

二、第二部分

Node 類:提供了 addChildremoveChild 方法來建立遊戲的節點樹。
Vector 類:封裝了對於對象的 retain 操做和 release 操做。

2.1 Node

// 添加節點
void Node::addChild(Node *child)
{
    CCASSERT( child != nullptr, "Argument must be non-nil");
    this->addChild(child, child->_localZOrder, child->_name); // 通過這個方法-->addChildHelper-->insertChild,完成retain操做
}

// 移除節點
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
    //
    if (_children.empty())
    {
        return;
    }

    // 
    ssize_t index = _children.getIndex(child);
    if( index != CC_INVALID_INDEX )
        this->detachChild( child, index, cleanup );//注意這個函數
}

// 插入節點
void Node::insertChild(Node* child, int z)
{
    _transformUpdated = true;
    _reorderChildDirty = true;
    _children.pushBack(child);// pushBack方法對節點進行了retain
    child->_setLocalZOrder(z);
}


// 剝離節點
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
    
    ...// 部分省略

    _children.erase(childIndex);// erase方法對節點進行了release
}

2.2 Vector

// 這裏僅展現與Node類相關的內存管理的部分

// 將對象入棧,引用+1
void pushBack(T object)
{
    CCASSERT(object != nullptr, "The object should not be nullptr");
    _data.push_back( object );

    object->retain(); // 進行了retain
}

// 將目標位置的對象移除
iterator erase(ssize_t index)
{
    CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
    auto it = std::next( begin(), index );

    (*it)->release(); // 進行了release

    return _data.erase(it);
}

到這裏,終於能夠把故事完整地講一遍了。內存管理的過程是怎麼樣的呢?首先,建立了一個 Node 對象A, Node 繼承 Ref,所以 Ref 的引用計數爲1;而後A又經過 autorelease 將本身放入自動釋放池;接着,有個 Node 對象B,B經過 addChild(A) 使得A的引用+1;幾個 mainLoop 後,B經過 removeChild(A) 使得A的引用-1;這個 mainLoopdrawScene() 完成後,一幀結束, Director 經過釋放池將池中的對象 clear(),即對 Node 對象A進行 release 操做。A的引用計數變爲0,執行 delete 釋放A對象。

三、高階用法

之因此稱爲高階用法,是由於,若是開發者對 Cocos 的內存管理機制理解不夠深入,那麼極可能會用錯而致使損失大於收益。另外一方面,這類用法在平時不多會用到。

3.1 使用 retain 來延長對象的生存時間

在開發過程當中,若是須要使用一個節點對象,可是又不想把它放到節點樹裏面去。那麼就可使用 retain 來避免對象被自動釋放掉。

3.2 使用 PoolManagerpush 來延長對象的生存時間

有些狀況下,但願閒置對象晚一幀進行銷燬,可使用 push 把當前釋放池推入棧底,那麼這一幀結束的時候只會釋放剛 push 進去的釋放池。

筆者自己尚未機會使用太高階用法,若是有小夥伴發現了高階用法在實際問題中的應用,敬請留言交流。

4、參考連接

深刻理解 Cocos2d-x 內存管理 (從這篇文章中得到許多啓發。)
Cocos2d-x源碼(位於Github上,若是連不上嘗試使用前綴 https)
引用計數——維基百科(關於引用計數的說明)
Cocos2d-x的內存管理機制概述(裏面提到了爲何要有 PoolManager)
cocos2dx 3.2 (24)——內存管理機制(編纂地比較詳細)

相關文章
相關標籤/搜索