通過上兩篇博客,我們對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了,花了一個週末連學習帶總結,累死寶寶了<@_@>