cocos2dx 3.1從零學習(四)——內存管理(錯誤案例分析)

本篇內容文字比較較多,但是這些都是建立在前面三章寫代碼特別是傳值的時候崩潰的基礎上的。可能表達的跟正確的機制有出入,還請指正。 如果有不理解的可以聯繫我,大家可以討論一下,共同學習。


首先明確一個事實,retainrelease是一一對應的,跟new和delete一樣


1.引用計數retain release

這裏請參考一下引用計數的書籍肯定說的比我講的詳細

簡單一點理解就是new的指針加一個計數器每引用一次這塊內存計數就加1。在析構的時候減1,如果等於0的時候就delete這個指針並置空

 

2.自動釋放autolease

autorelease後的對象默認計數是1,並且autorelease的對象會被放到自動釋放池裏自動釋放池這裏有一個需要注意的地方自動釋放池存儲了當前幀所有的autorelease的對象在幀結束時對其中所有對象release一次處理完後這個釋放池就不再擁有對這些對象的處理權也就是說自動釋放池只會最其中的對象進行一次release操作釋放的同時使用一個新的釋放池存儲後一幀定義的autorelease對象,如此循環下去。

 

精靈們create函數執行後會被放到自動釋放池釋放池會在每幀結束的時候調用對於引用計數爲1的內存進行釋放如果沒有其他操作比如retain或者addchild的話那麼引用計數沒有增加當前幀結束後計數減10這個指針也就不復存在了


什麼時候計數會加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");創建的精靈的整個生命週期的引用計數進行分析


主場景createautorelease(1)->retain(2)->autorelease自動釋放池release(1)->在子場景中被addchild(2)->子場景析構的鏈式反應(1)->???

請看子場景析構的時候計數還是1,這會造成內存泄露所以我們應該在析構函數中執行一次sp->release().手動減1。

 

CC_SYNTHESIZE_RETAIN的出現就是爲了解決上述問題它只是把retainrelease操作包裝了一下

這個時候你再去看一遍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。

 

 

總結:retainrelease是一一對應的但是我們應該使用它們的加強版宏定義CC_SAFE_RETAINCC_SAFE_RELEASE。這兩個可不是一一對應的比如我們 CC_SYNTHESIZE_RETAIN定義的變量只在析構函數中加一句CC_SAFE_RELEASE。

發佈了63 篇原創文章 · 獲贊 56 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章