【深入瞭解cocos2d-x 3.x】UI樹(2)——UI樹的內存管理機制

上篇文章分析了什麼是UI樹,以及UI樹的使用方法,這節會重點分析UI樹的內存管理機制以及如何利用UI樹對遊戲中的UI內存進行合理的管理。


說到UI樹的內存管理機制,就不得不提cocos2d-x的內存管理機制——引用計數了,相信只要不是初學者都已經理解了這一塊了,這裏還是對cocos2d-x的內存管理機制做一個大概的介紹吧。

cocos2d-x採用的是引用計數法作爲其內存管理的方法,引用計數法的核心思想爲,當某個類需要引用變量x時,需要增加一個變量x的引用計數,當這個類不需要變量x時,需要減少一個變量x的引用計數。這樣,誰引用,誰釋放,引用和釋放成對出現,就可以避免掉內存泄露的問題了。cocos2d-x對這一方法還有一個擴展,就是自動延遲釋放機制,就是,如果存在一個變量x,它在函數的一幀上都需要用,但是下一幀,變量x就可以被釋放掉,如果我們手動的在下一幀上釋放x,操作其他會非常麻煩,也不直觀,cocos2d-x提供了一個函數:autorelease,這個函數將會把對象加入到默認的自動釋放池中,在一幀結束,引擎將自動清理自動釋放池中的變量內存,這樣就非常方便了。

cocos2d-x有一個不成文的規定,指針的創建一般不會用new直接創建,而是通過一個方法:create來創建,這個方法已經被引擎封裝成一個宏定義了:CREATE_FUNC,下面是這個宏定義的實現:

#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
    __TYPE__ *pRet = new __TYPE__(); \
    if (pRet && pRet->init()) \
    { \
        pRet->autorelease(); \
        return pRet; \
    } \
    else \
    { \
        delete pRet; \
        pRet = NULL; \
        return NULL; \
    } \
}

其他函數我們不分析,可以看到它在其中首先new了這個類__TYPE__, 這時候new出來的對象的引用計數爲1,然後初始化完成後,這裏執行了autorelease,這時候引用計數仍然爲1,但是引擎將其加入了自動釋放池,在這一幀結束的時候,這個對象的引用計數將變爲0,引用計數爲0的對象將會被釋放掉。


上述很囉嗦的介紹了一下cocos2d-x的內存管理機制,現在進入正文了,當一個節點被加入到UI樹中,它的引用計數將會有怎麼樣的變化呢?下面是Node的addChild的源碼分析(addChild中真正的實現在addChildHelper中,下文忽略了不相關的代碼):

void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
    this->insertChild(child, localZOrder);
    
    if (setTag)
        child->setTag(tag);
    else
        child->setName(name);
    
    child->setParent(this);
    child->setOrderOfArrival(s_globalOrderOfArrival++);
}

可以看到真正的實現是在insertChild這個函數中的,我們繼續尾隨進去:

void Node::insertChild(Node* child, int z)
{
    _transformUpdated = true;
    _reorderChildDirty = true;
    _children.pushBack(child);
    child->_setLocalZOrder(z);
}
好艱辛,終於看到了什麼,這裏將child加入到了_children中,_children是什麼呢? 看它的聲明

Vector<Node*> _children;
注意,這是一個大寫V開頭的Vector,說明這是cocos2d-x自己實現的可變數組,這個數組實際上和std標準庫中的數組的實現差不多,標準庫的算法可以完美的應用在這個數組上,這個數組與std::vector的最大區別就是引入了引用技術機制。在pushBack中,究竟做了些什麼呢?

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


沒錯,重點在於這裏

object->retain();

它對於添加進來的對象都增加了引用,這樣就說明,所有被加入UI樹中的節點都會被UI樹保持強引用。


接下來對於removeXXXX函數進行分析,就挑選removeChild函數進行分析吧(其他函數也大同小異)

void Node::removeChild(Node* child, bool cleanup /* = true */)
{
    // explicit nil handling
    if (_children.empty())
    {
        return;
    }

    ssize_t index = _children.getIndex(child);
    if( index != CC_INVALID_INDEX )
        this->detachChild( child, index, cleanup );
}

而這個函數最終調用的是 detachChild函數,來繼續跟蹤進去吧(忽略的無關代碼)

void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
    // set parent nil at the end
    child->setParent(nullptr);

    _children.erase(childIndex);
}

這裏的重點代碼就是

_children.erase(childIndex);

同樣跟蹤進入看看它的實現:

    iterator erase(ssize_t index)
    {
        CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
        auto it = std::next( begin(), index );
        (*it)->release();
        return _data.erase(it);
    }
沒錯,它執行了下面這句代碼:

(*it)->release();
減少了對象的引用計數,這樣就能將UI從UI樹中分離並且不會造成內存泄露了。


當然,這樣做的好處還不止這些,試想如下代碼

Scene* s = Scene::create();
Director::getInstance()->runWithScene(s);
Layer* l = Layer::create();
s->addChild(l);

.... 若干幀後

s->removeChild(l);


是否會造成內存泄露?

答案是不會,而且這樣寫出來的代碼,我們並不需要關心內存的分配問題,引擎會自動幫我們申請內存,並且在不需要的時候,自動將內存回收。這似乎是一個非常好的解決方案,但是也有一些不足。

試想如下使用場景,現需要將上述的l節點與s節點中間增加一個層m,m是s場景的子節點,也是l層的父節點,這時候應該怎麼做呢?要知道在removeChild之後,l層的內存已經被釋放掉了。似乎沒有什麼解決方法了,看下文:

Scene* s = Scene::create();
Director::getInstance()->runWithScene(s);
Layer* l = Layer::create();
l->setTag(1);
s->addChild(l);

.... 若干幀後

auto l = s->getChildByTag(1);
l->retain();
s->removeChild(l);
Layer* m = Layer::create();
s->addChild(m);
m->addChild(l);
l->release();

這樣提前將l取出來增加一個引用計數就可以避免l的內存被UI樹釋放掉了,但是值得注意的是,retain方法必須與release方法對應出現,否則會造成內存泄露。但是開發者往往會忘記寫後面的release從而造成內存泄露,那麼怎麼避免這樣的情況出現呢,答案是:智能指針。看下面的代碼

Scene* s = Scene::create();
Director::getInstance()->runWithScene(s);
Layer l = Layer::create();
l->setTag(1);
s->addChild(l);

.... 若干幀後

RefPtr<Node*> l = s->getChildByTag(1);
s->removeChild(l);
Layer m = Layer::create();
s->addChild(m);
m->addChild(l);

非常簡單方便,完全不需要關心內存的申請和釋放,關於智能指針部分以後會對其做出分析。




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