本篇內容文字比較較多,但是這些都是建立在前面三章寫代碼特別是傳值的時候崩潰的基礎上的。可能表達的跟正確的機制有出入,還請指正。 如果有不理解的可以聯繫我,大家可以討論一下,共同學習。
首先明確一個事實,retain和release是一一對應的,跟new和delete一樣。
1.引用計數retain release
這裏請參考一下引用計數的書籍,肯定說的比我講的詳細。
簡單一點理解就是,對new的指針加一個計數器,每引用一次這塊內存,計數就加1。在析構的時候減1,如果等於0的時候就delete這個指針並置空。
2.自動釋放autolease
autorelease後的對象默認計數是1,並且autorelease的對象會被放到自動釋放池裏。自動釋放池這裏有一個需要注意的地方,自動釋放池存儲了當前幀所有的autorelease的對象,在幀結束時對其中所有對象release一次,處理完後這個釋放池就不再擁有對這些對象的處理權,也就是說自動釋放池只會最其中的對象進行一次release操作。釋放的同時使用一個新的釋放池存儲後一幀定義的autorelease對象,如此循環下去。
精靈們create函數執行後會被放到自動釋放池,釋放池會在每幀結束的時候調用,對於引用計數爲1的內存進行釋放。如果沒有其他操作比如retain或者addchild的話,那麼引用計數沒有增加,當前幀結束後計數減1爲0後,這個指針也就不復存在了。
什麼時候計數會加1?
手動調用retain使引用技術加1;
cocos2dx我所見過的create靜態方法都是調用autorelease的,計數默認爲1。
每引用一次,比如使用頻率最多的addChild()會使其引用技術加1。
什麼時候計數會減1?
手動調用release使引用技術減1;
自動釋放池裏的會在當前幀結束的時候減1。注意是當前幀,後面的釋放池裏存儲的是後面幀運行時定義的autorelease對象。
如果一個場景析構,會對所有的子節點release一次,這被稱爲鏈式反應。
鏈式反應解釋如下:我們當前運行這一個場景,場景初始化,添加了很多層,層裏面有其它的層或者精靈,而這些都是 CCNode節點,以場景爲根,形成一個樹形結構,場景初始化之後(一幀之後),這些節點將完全 依附 (內部通過 retain) 在這個樹形結構之上,全權交由樹來管理,當我們 砍去一個樹枝,或者將樹 連根拔起,那麼在它之上的“子節點”也會跟着去除(內部通過release),這便是鏈式反應。來自 <http://www.tairan.com/archives/4184>
錯誤案例:
我們在create後,如果不使用retain使引用計數加1的話,那麼自動釋放池會使其引用計數減1,如果在回調函數中使用addchild(sp)會崩潰。
要想解決這個問題,在create後添加使用sp->retain();來增加它的引用計數。
如下:
auto temp = Sprite::create("CloseNormal.png");
temp->retain();//如果註釋掉會崩潰。
auto item4 =MenuItemLabel::create(Label::createWithBMFont("fonts/futura-48.fnt","Hell"), \
[=](Ref * ref){
addChild(temp);
});
有些人可能會使用引用的lambda表達式,如下:
auto temp =Sprite::create("CloseNormal.png");
temp->retain();
auto item4 =MenuItemLabel::create(Label::createWithBMFont("fonts/futura-48.fnt","Hell"), \
[&](Ref * ref){
addChild(temp);
});
崩潰了!引用的話 即使retain也會崩潰,這個爲什麼呢?
引用的話我們使用的是temp的別名引用,也就指向指針的指針temp。當這個函數執行完的時候temp做爲局部變量就會被釋放。所以我們在回調函數中使用的temp已經不存在了。 如果是=賦值的話,精靈的指針會拷貝一份傳到lambda表達式中,所以不會崩潰。
要想解決引用崩潰的問題,我們只要使temp不會被釋放就好。所以定義爲成員變量可以解決引用的lambda表達式造成的問題,大家可以嘗試一下。
深入理解CC_SYNTHESIZE_RETAIN
假裝我們從未學習過CC_SYNTHESIZE_RETAIN。第二篇講過場景之間的正向傳值,如果我們在主場景create一個精靈,然後賦值給下一個場景的成員變量Sprite *sp,對於這種autorelease的變量我們應該怎麼進行傳值操作呢?
autorelease變量會在每一幀結束的時候計數減1進行銷燬。所以我們應該對其計數加1,避免下個場景使用的時候已經被刪除。
我們應該在主場景切換場景的時候這樣寫:
voidMainScene::Morning_0623_MemoryManage(cocos2d::Ref * ref)
{
auto scene = MemoryManage::createScene();
auto memLayer = (MemoryManage *)scene->getChildren().at(0);
tmpSp =Sprite::create("coc/buildings_lowres/59.0.png");//注意斜槓的方向
tmpSp->retain();//引用計數加1,否則當前幀結束會被銷燬
memLayer->sp = tmpSp;//如果不retain的話會被自動釋放掉 在切換場景的時候會被釋放掉。
Director::getInstance()->pushScene(scene);
}
在下個場景MemoryManage定義成員變量sp的時候應該對其進行初始化,因爲它是一個指針。
我們應該定義Sprite *sp = nullptr;
否則在MainScene複製的時候會崩潰,因爲它的一個未知的指針,指向了內存中未知的區域。
崩潰的地方如下:
斷言失敗 CCASSERT(_referenceCount > 0,"reference count should greater than 0");
因爲這個時候sp是一個未知的指針。
下面我們對主場景中
tmpSp =Sprite::create("coc/buildings_lowres/59.0.png");創建的精靈的整個生命週期的引用計數進行分析。
主場景create時autorelease(1)->retain(2)->autorelease自動釋放池release(1)->在子場景中被addchild(2)->子場景析構的鏈式反應(1)->???
請看子場景析構的時候計數還是1,這會造成內存泄露。所以我們應該在析構函數中執行一次sp->release().手動減1。
CC_SYNTHESIZE_RETAIN的出現就是爲了解決上述問題,它只是把retain和release操作包裝了一下。
這個時候你再去看一遍CC_SYNTHESIZE_RETAIN的源碼:
#defineCC_SYNTHESIZE_RETAIN(varType, varName, funName) \
private: varTypevarName; \
public: virtualvarType get##funName(void) const { return varName; } \
public: virtual voidset##funName(varType var) \
{ \
if (varName != var) \
{ \
CC_SAFE_RETAIN(var); \
CC_SAFE_RELEASE(varName); \
varName = var; \
} \
}
調用CC_SYNTHESIZE_RETAIN來給成員變量賦值時,會對原來的變量進行一次retain操作。然後需要我們在析構函數的時候添加對應的 CC_SAFE_RELEASE(varName);
現在說一下爲什麼在CC_SYNTHESIZE_RETAIN中對成員變量varName執行CC_SAFE_RELEASE(varName);
varName如果被不同的變量多次賦值會怎麼樣? 每一次的賦值原來的變量都要做一次retain操作,如果我們直接改變了varName的值而不改變它原來指向的內存的引用計數的話,那麼就會造成內存泄露。 所以每次賦值都會對原來的內存進行一次release。
總結:retain和release是一一對應的,但是我們應該使用它們的加強版。宏定義CC_SAFE_RETAIN和CC_SAFE_RELEASE。這兩個可不是一一對應的。比如我們 CC_SYNTHESIZE_RETAIN定義的變量,只在析構函數中加一句CC_SAFE_RELEASE。