Ref類是Cocos2d-x根類,Cocos2d-x中的不少類都派生自它,例如,咱們熟悉的節點類Node也派生自Ref。咱們介紹Ref內存管理。
內存引用計數
Ref類設計來源於Cocos2d-iphone的CCObject類,在Cocos2d-x 2.x中也叫CCObject類。所以Ref類的內存管理是參考Objective-C手動管理引用計數(Reference Count)而設計的。
如圖所示是內存引用計數原理示意圖。
html
每一個Ref對象都有一個內部計數器,這個計數器跟蹤對象的引用次數,被稱爲「引用計數」(Reference Count,簡稱RC)。當對象被建立時候,引用計數爲1。爲了保證對象的存在,能夠調用retain函數保持對象,retain會使其引用計數加1,若是不須要這個對象能夠調用release函數,release使其引用計數減1。當對象的引用計數爲0的時候,引擎就知道再也不須要這個對象了,就會釋放對象內存。
引用計數實例如圖所示,咱們在ObjA中使用new等操做建立了一個Ref對象,這時候這個對象引用計數爲1。而後在OjbB中使用retain函數保持Ref對象,這時引用計數爲2。再而後ObjA中調用release函數,這時引用計數爲1。在ObjB中調用release函數,這時引用計數爲0。這個時候Ref對象就會由引擎釋放。
安全
在Ref類中相關函數有:retain()、release()、autorelease()和getReferenceCount()。其中autorelease()函數與release()函數相似,它會延後使引用計數減1,autorelease()咱們稍後再介紹。getReferenceCount()函數返回當前的引用計數。
自動釋放池
咱們先看看下面的代碼片斷。
微信
[html] view plaincopyapp
XmlParser * XmlParser::createWithFile(const char *fileName) iphone
{ 函數
XmlParser *pRet = new XmlParser(); 網站
// ① this
return pRet; spa
} .net
上述代碼XmlParser::createWithFile(const char *fileName)函數可以建立XmlParser對象指針並返回給調用者。根據咱們前面介紹的C++使用new規則,在XmlParser::createWithFile函數中還應該釋放對象的語句,若是沒有,那麼每次調用者調用XmlParser::createWithFile函數都會建立一個新對象,老的對象沒有釋放,就會形成內存泄漏。可是若是咱們在第①行代碼,添加釋放語句pRet->release(),那麼問題可能會更嚴重,返回的對象可能已經被釋放,返回的多是一個野指針。
自動釋放池(AutoReleasePool)正是爲此而設計,自動釋放池也是來源於Objective-C,Cocos2d-x中維護AutoreleasePool對象,它可以管理即將釋放的對象池。咱們在第①可使用pRet->autorelease()語句,autorelease()函數將對象放到自動釋放池,但對象的引用計數並不立刻減1,而是要等到一個消息循環結束後減1,若是引用計數爲0(即,沒有被其它類或Ref對象retain),則釋放對象,在此以前對象並不會釋放。
消息循環是遊戲循環一個工做職責,消息循環說到底仍是遊戲循環,消息循環是接收事件,並處理事件。自動釋放池的生命週期也是由消息循環管理的。如圖所示,圖中「圈圈」是消息循環週期,它的一個工做職責是維護自動釋放池建立和銷燬。每次爲了處理新的事件,Cocos2d-x引擎都會建立一個新的自動釋放池,事件處理完成後,就會銷燬這個池,池中對象的引用計數會減1,若是這個引用計數會減0,也就是沒有被其它類或Ref對象retain,則釋放對象,不然這個對象不會釋放,在此次銷燬池過程當中「倖存」下來,它被轉移到下一個池中繼續生存。
下面咱們看一個實例,下面代碼是13.2.2一節的實例HelloWorldScene.cpp代碼:
[html] view plaincopy
bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
Size visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
auto goItem = MenuItemImage::create(
"go-down.png",
"go-up.png",
CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
goItem->setPosition(Vec2(origin.x + visibleSize.width - goItem->getContentSize().width/2 ,
origin.y + goItem->getContentSize().height/2));
auto menu = Menu::create(goItem, NULL); ①
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1); ②
this->list = __Array::createWithCapacity(MAX_COUNT);
this->list->retain(); ③
for(int i = 0;i < MAX_COUNT; ++i){
Sprite* sprite = Sprite::create("Ball.png");
this->list->addObject(sprite); ④
}
return true;
}
void HelloWorld::menuCloseCallback(Ref* pSender)
{
Ref* obj = NULL;
log("list->count() = %d",this->list->count()); ⑤
Size visibleSize = Director::getInstance()->getVisibleSize();
CCARRAY_FOREACH(this->list, obj) {
Sprite* sprite = (Sprite*)obj; ⑥
int x = CCRANDOM_0_1() * visibleSize.width;
int y = CCRANDOM_0_1() * visibleSize.height;
sprite->setPosition( Vec2(x, y) );
this->removeChild(sprite);
this->addChild(sprite);
}
}
HelloWorld::~HelloWorld()
{
this->list->removeAllObjects();
CC_SAFE_RELEASE_NULL(this->list); ⑦
}
在上述代碼中咱們須要關注兩個對象(Menu和__Array)建立。第①行auto menu = Menu::create(goItem, NULL)經過create靜態工廠建立對象,關於靜態工廠的建立原理咱們會在下一節介紹。若是咱們不採用第②行的this->addChild(menu, 1)語句將menu 對象放入到當前層(HelloWorld)的子節點列表中,那麼這個menu對象就會在當前消息循環結束的時候被釋放。調用this->addChild(menu, 1)語句會將它的生命週期持續到HelloWorld層釋放的時候,而不會在當前消息循環結束釋放。
菜單、層等節點對象能夠調用addChild函數,使得其生命延續。並且__Array和__Dictionary等Ref對象沒有調用addChild函數保持,咱們須要顯式地調用retain函數保持它們,以便延續其生命。如代碼第③行this->list->retain(),list就是一個__Array指針類型的成員變量,若是沒有第③行語句,那麼在第⑤行代碼this->list->count()程序就會出錯,由於這個時候list對象已經釋放了。採用了retain保持的成員變量,必定要release(或autorelease),retain和release(或autorelease)必定是成對出現的。咱們能夠在析構函數~HelloWorld()中調用release釋放,而第⑦行代碼CC_SAFE_RELEASE_NULL(this->list)就是實現這個目的,其中CC_SAFE_RELEASE_NULL宏做用以下:
list->release();
list = nullptr;
可見CC_SAFE_RELEASE_NULL宏不只僅釋放對象,還將它的指針設置爲nullprt[],也樣能夠防止野指針。
上述代碼還有一個很是重要的關於內存的問題,咱們在HelloWorld::init()函數中建立了不少Sprite對象,經過第④行代碼this->list->addObject(sprite)將它們放到list容器對象中,它們沒有調用addChild函數也沒有顯式retain函數,然而在當前消息循環結束後它們仍是「存活」的,因此在第⑥行Sprite* sprite = (Sprite*)obj中,取出的對象是有效的。這個緣由__Array和__Dictionary等容器對象的add相關函數可使添加對象引用計數加1,相反的remove相關函數可使添加對象引用計數減1。
Ref內存管理規則
下面咱們給出使用Ref對象時候,內存管理一些基本規則:
一、在使用Node節點對象時候,addChild函數能夠保持Node節點對象,使引用計數加1。經過removeChild函數移除Node節點對象,使引用計數減1。它們都是隱式調用的,咱們不須要關心它們的內存管理。這也正是爲何在前面的章節中咱們無數次地使用了Node節點對象,而歷來都沒有擔憂過它們內存問題。
二、若是是__Array和__Dictionary等容器對象,能夠經過它們add相關函數添加元素會使引用計數加1,相反的remove相關函數刪除元素會使引用計數減1。可是前提是__Array和__Dictionary等容器對象自己不沒有被釋放。
三、若是不屬於上面提到的Ref對象,須要保持引用計數,能夠顯式調用retain函數使引用計數加1,而後顯式調用release(或autorelease)函數使引用計數減1。
四、每一個 retain函數必定要對應一個 release函數或一個 autorelease函數。
五、release函數使得對象的引用計數立刻減1,這是所謂的「斬立決」,可是是否真的釋放掉內存要看它的引用計數是否爲0。autorelease函數只是在對象上作一個標記,等到消息循環結束的時候再減1,這是所謂的「秋後問斬」,在「秋天」沒有到來以前,它的內存必定沒有釋放,能夠安全使用,可是「問斬」以後,是否真的釋放掉內存要看它的引用計數是否爲0。所以不管是那一種方法,引用計數是爲0纔是釋放對象內存的條件。下面的代碼是Ref類的release函數,經過這段代碼能夠幫助咱們理解引用計數。
[html] view plaincopy
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
auto poolManager = PoolManager::getInstance();
if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
{
// Trigger an assert if the reference count is 0 but the Ref is still in autorelease pool.
// This happens when 'autorelease/release' were not used in pairs with 'new/retain'.
//
// Wrong usage (1):
//
// auto obj = Node::create(); // Ref = 1, but it's an autorelease Ref which means it was in the autorelease pool.
// obj->autorelease(); // Wrong: If you wish to invoke autorelease several times, you should retain `obj` first.
//
// Wrong usage (2):
//
// auto obj = Node::create();
// obj->release(); // Wrong: obj is an autorelease Ref, it will be released when clearing current pool.
//
// Correct usage (1):
//
// auto obj = Node::create();
// |- new Node(); // `new` is the pair of the `autorelease` of next line
// |- autorelease(); // The pair of `new Node`.
//
// obj->retain();
// obj->autorelease(); // This `autorelease` is the pair of `retain` of previous line.
//
// Correct usage (2):
//
// auto obj = Node::create();
// obj->retain();
// obj->release(); // This `release` is the pair of `retain` of previous line.
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
delete this;
}
}
六、一個對象調用autorelease函數,它就會將對象放到自動釋放池裏,它生命週期自動釋放池生命週期息息相關,池在消息循環結束的時候會釋放,池會調用池內對象release函數,使得它們的引用計數減1。下面的代碼是AutoreleasePool類的清除函數clear(),經過這段代碼能夠幫助咱們理解自動釋放池機制。
[html] view plaincopy
void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;
#endif
for (const auto &obj : _managedObjectArray)
{
obj->release();
}
_managedObjectArray.clear();
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;
#endif
}
更多內容請關注國內第一本Cocos2d-x 3.2版本圖書《Cocos2d-x實戰:C++卷》
本書交流討論網站:http://www.cocoagame.net
更多精彩視頻課程請關注智捷課堂Cocos課程:http://v.51work6.com
歡迎加入Cocos2d-x技術討論羣:257760386
歡迎關注智捷iOS課堂微信公共平臺