Cocos2d-x 內存管理淺說

本文出自[無間落葉](轉載請保留出處):http://blog.leafsoar.com/archives/2013/05-22-23.html

使用過 Cocos2d-x 都知道,其中有一套自己實現的內存管理機制,不同於一般 C++ 的編寫常規,而在使用前,瞭解其原理是有必要的,網上已經有很多對內部實現詳細解說的文章。而對於使用者而言,並不需要對其內部有很深的瞭解,注重其“機制”,而非內部實現,在這裏只是簡單的聊一聊它的管理方式以及使用,固爲淺說。

無用對象 與 管理對象

Cocos2d-x 將會在下一幀自動清理無用的對象,什麼是無用的對象,通過 create() 方法創建的就是無用的對象。

爲了簡要說明,代碼的組織設計一切從簡,我們創建了兩個輔助類和一個容器類 BaseLayer,在 BaseLayer 之上管理內部對象,並觀察它是怎麼自動管理對象的。實現了其 構造函數 方法和 析構函數,並做些日誌打印,以方便我們觀察:

class LSLayer: public CCNode {
public:
    virtual bool init() {
        CCLog("LSLayer().init()");
        return true;
    };

    CREATE_FUNC(LSLayer);

    LSLayer(){
        CCLog("LSLayer().()");
    };
    ~LSLayer() {
        CCLog("LSLayer().~()");
    };
};

class LSSprite: public CCNode {
public:
    virtual bool init() {
        CCLog("LSSprite().init()");
        return true;
    };

    CREATE_FUNC(LSSprite);

    LSSprite(){
        CCLog("LSSprite().()");
    };
    ~LSSprite() {
        CCLog("LSSprite().~()");
    };
};

class BaseLayer: public CCLayer {
public:
    virtual bool init(){
        CCLog("BaseLayer().init()");
        // 我們創建了兩個 “無用”對象
        LSLayer* layer = LSLayer::create();
        LSSprite* sprite = LSSprite::create();
        // 使用了 layer 變爲受“管理”的對象
        this->addChild(layer);

        return true;
    };

    CREATE_FUNC(BaseLayer);

    BaseLayer(){
        CCLog("BaseLayer().()");
    };
    ~BaseLayer(){
        CCLog("BaseLayer().~()");
    };
};

如上所示,我們在 BaseLayer 中創建了兩個對象, layer 和 sprite,而只使用了 layer ,如果要運行上面的 BaseLayer 代碼,我們需要創建一個 BaseLayer 的層對象,並將它添加到運行的場景或者層中: addChild(BaseLayer::create());,以保證 BaseLayer 開始運行,現在我們分析一下運行的結果:

// 由 addChild(BaseLayer::create()); 方法開始,創建並初始化了 BaseLayer 層
cocos2d-x debug info [BaseLayer().()]
cocos2d-x debug info [BaseLayer().init()]
// BaseLayer init 方法我們創建了兩個對象
cocos2d-x debug info [LSLayer().()]
cocos2d-x debug info [LSLayer().init()]
cocos2d-x debug info [LSSprite().()]
cocos2d-x debug info [LSSprite().init()]
// 對象創建完成,緊接着這“無用”對象便已經釋放了,而另一個已經使用的對象沒有釋放
cocos2d-x debug info [LSSprite().~()]

通過上面兩個例子對比,對 cocos2d-x 的對象管理有了初步的認識,它會自動清理 “無用對象”。爲了區分概念,我們將另一種對象稱之爲 “管理對象”,它是受管理的,有用的對象。比如上文中的 layer

!!這也算初步認識,當然,這至少解決了我們這樣一個疑問:我們在場景初始化的時候,通過 create() 創建了成員變量,以備需要的時候使用,但發現在使用的時候這個對象已經不存在了,從而導致程序崩潰。

管理對象不用之時立即回收

我們再繼續演變 BaseLayer 的實現,以方便我們觀察在每一幀對象的情況,添加實現了定時器功能:

class BaseLayer2: public CCLayer {
public:
    virtual bool init(){
        CCLog("BaseLayer2().init()");
        // 啓用定時器,自動在每一幀調用 update 方法
        this->scheduleUpdate();
        return true;
    };

    // 定義 update 統計
    int updateCount;
    LSLayer* layer;
    LSSprite* sprite;

    virtual void update(float fDelta){
        // 爲了方便觀察,不讓 update 內部無止境的打印下去
        if (updateCount < 3){
            updateCount ++;
            CCLog("update index: %d", updateCount);

            // 在不同的幀做相關操作,以便觀察
            if (updateCount == 1){
                layer = LSLayer::create();
                this->addChild(layer);
                sprite = LSSprite::create();

            } else if (updateCount == 2){
                this->removeChild(layer, true);

            } else if (updateCount == 3){

            }

            CCLog("update index: %d end", updateCount);
        }
    };

    CREATE_FUNC(BaseLayer2);

    BaseLayer2():
        updateCount(0),
        layer(NULL),
        sprite(NULL)
    {
        CCLog("BaseLayer2().()");
    };
    ~BaseLayer2(){
        CCLog("BaseLayer2().~()");
    };
};

// 打印如下
cocos2d-x debug info [BaseLayer2().()]
cocos2d-x debug info [BaseLayer2().init()]
// 第一幀創建兩個對象
cocos2d-x debug info [update index: 1]
cocos2d-x debug info [LSLayer().()]
cocos2d-x debug info [LSLayer().init()]
cocos2d-x debug info [LSSprite().()]
cocos2d-x debug info [LSSprite().init()]
cocos2d-x debug info [update index: 1 end]
// 我們看到 sprite 無用對象在 第一幀和第二幀之間被釋放
cocos2d-x debug info [LSSprite().~()]
cocos2d-x debug info [update index: 2]
// 在第二幀移除管理對象,可以看到它是立即釋放,在 index: 2 end 之前
cocos2d-x debug info [LSLayer().~()]
cocos2d-x debug info [update index: 2 end]
cocos2d-x debug info [update index: 3]
cocos2d-x debug info [update index: 3 end]

與無用對象不同的是,管理對象在不用之時,立即釋放,這決定着如果想在其它地方使用此對象,在“完全”不用之前,一定要有所作爲。重寫 update 方法如下:

virtual void update(float fDelta){
    // 爲了方便觀察,不讓 update 內部無止境的打印下去
    if (updateCount < 3){
        updateCount ++;
        CCLog("update index: %d", updateCount);

        // 在不同的幀做相關操作,以便觀察
        if (updateCount == 1){
            layer = LSLayer::create();
            this->addChild(layer);
            sprite = LSSprite::create();
            CCLog("%d", layer);
        } else if (updateCount == 2){
            layer->retain();
            this->removeChild(layer, true);
            CCLog("%d", layer);
        } else if (updateCount == 3){
            layer->release();
            if (layer){
                CCLog("%d", layer);
            }
        }

        CCLog("update index: %d end", updateCount);
    }
};

/// 打印如下
cocos2d-x debug info [update index: 1]
cocos2d-x debug info [LSLayer().()]
cocos2d-x debug info [LSLayer().init()]
cocos2d-x debug info [LSSprite().()]
cocos2d-x debug info [LSSprite().init()]
cocos2d-x debug info [147867424]
cocos2d-x debug info [update index: 1 end]
cocos2d-x debug info [LSSprite().~()]
cocos2d-x debug info [update index: 2]
// 第二幀並沒有釋放 layer,因爲它還是有用的管理對象
cocos2d-x debug info [147867424]
cocos2d-x debug info [update index: 2 end]
cocos2d-x debug info [update index: 3]
// 完全棄用,立即釋放    
cocos2d-x debug info [LSLayer().~()]
// 但是 layer 對象的地址還是可用的
cocos2d-x debug info [147867424]
cocos2d-x debug info [update index: 3 end]

在完全不用之前,要有所作爲。 如果我們將第二幀中的 layer->retain(); 放在 this->removeChild(layer, true); 之後呢,我們知道在 removeChild 之後是立即釋放的,此時 layer 對象已經不存在了,而 layer 所指向的內存地址是個無效地址。如果你的程序繼續運行,那麼一定會出現內存錯誤。

如果程序直接錯誤異常退出,倒也罷了,怕就怕,程序可能繼續運行,layer 雖然是無效地址,但並不是 NULL,可能所指向的地址可用,可能還能繼續執行,更可能的還能繼續 layer->retain(); 操作。這會影響我們的判斷,程序真的有問題麼。如果留下了這種隱患,那麼排除錯誤的難度會大大加深。比如程序莫名其妙的退出,時好時壞!(經過一葉的測試,這種情況是可能發生的,而且頻率相當高,測試平臺:Linux 平臺,Android平臺可能性稍低)

第三幀我們通過 if (layer) 判斷對象是否可用,如果可用我們繼續操作 layer ,這樣的使用方式也將會留下內存隱患,因爲這樣的判斷是能通過的,但卻是 不一定 能夠正確使用的。

一般而言,我們不一定需要 if(layer) 諸如此類的判斷,這也是不推薦的。管理對象,誰使用,那麼誰就是可控的!如果在對象銷燬之前 誰 retain() ,那麼在 release() 之前,它無需判斷即可使用。誰 addXXX 使用,一般能通過 getXXX 獲取。

簡而言之,誰使用(引用),你就找誰就行了,不論是獲取,或者移除。

我們前面所言,管理對象不用之時,立即回收,那麼我們在同一幀使用,然後移除呢?我們繼續改寫 update 方法,驗證想法:

virtual void update(float fDelta){
    // 爲了方便觀察,不讓 update 內部無止境的打印下去
    if (updateCount < 3){
        updateCount ++;
        CCLog("update index: %d", updateCount);

        // 在不同的幀做相關操作,以便觀察
        if (updateCount == 1){
            layer = LSLayer::create();
            this->addChild(layer);
            this->removeChild(layer, true);
        } else if (updateCount == 2){

        } else if (updateCount == 3){

        }

        CCLog("update index: %d end", updateCount);
    }
};

/// 其打印如下
cocos2d-x debug info [update index: 1]
cocos2d-x debug info [LSLayer().()]
cocos2d-x debug info [LSLayer().init()]
cocos2d-x debug info [update index: 1 end]
// layer 在兩幀之間釋放,也既是在一下幀自動清理
cocos2d-x debug info [LSLayer().~()]
cocos2d-x debug info [update index: 2]
cocos2d-x debug info [update index: 2 end]

這裏我們在同一幀 addChild 並且隨之 removeChild,那麼 layer 的性質又是如何,我們知道 管理對象 在不用之時會立即釋放,但在這裏並沒有立即釋放,那說明什麼,說明 layer 並不是管理對象,還只是無用對象,並且在這一幀結束時,或者說在 幀過度 的時候,並沒有使用,可想而知,在 幀過度的時候,其內部做了些處理,首先自動清理無用對象,或者將以使用的無用對象變成管理對象,而在以後的幀,如果在管理對象不用之時,將會立即釋放。

現在來看一看稍微複雜點的結構會如何。

// 在不同的幀做相關操作,以便觀察
if (updateCount == 1){
    layer = LSLayer::create();
    sprite = LSSprite::create();
    layer->addChild(sprite);
    addChild(layer);
} else if (updateCount == 2){

    this->removeChild(layer, true);
} else if (updateCount == 3){

}

/// 打印如下
cocos2d-x debug info [update index: 2]
cocos2d-x debug info [LSLayer().~()]
cocos2d-x debug info [LSSprite().~()]
cocos2d-x debug info [update index: 2 end]

我們創建了兩個對象 layer 和 sprite,將 sprite 添加到 layer,並把通過 addChild(layer) 使用 layer,可以看到,在第二幀移除 layer 的時候,立即釋放了 layer 和 sprite 對象。這也是 cocos2d-x 自動管理所實現的功能,在 使用者 不用的時候,它也將會解除對其它對象的使用。

基於以上情況,做些變形:

// 在不同的幀做相關操作,以便觀察
if (updateCount == 1){
    layer = LSLayer::create();
    sprite = LSSprite::create();
    layer->addChild(sprite);
} else if (updateCount == 2){
    this->removeChild(layer, true);
} else if (updateCount == 3){

}

/// 打印如下
cocos2d-x debug info [update index: 1 end]
cocos2d-x debug info [LSLayer().~()]
cocos2d-x debug info [LSSprite().~()]
cocos2d-x debug info [update index: 2]
cocos2d-x debug info [update index: 2 end]

創建了兩個對象 layer 和 sprite,將 sprite 添加到 layer 之中,而對 layer 不做處理,我們知道 layer 在第一幀結束後,會自動釋放,所以也會釋放其所引用的 sprite,而此時 sprite 的性質就有點微妙了。它在幀過度之間是怎麼處理的,它是不是我們這所說的無用對象呢?哈!如果 layer 首先被自動管理,那麼它會首先回收,並取消對 sprite 的引用,那麼 sprite 就是個無用對象,被自動回收。如果 sprite 首先被自動管理,那麼它將會先變成一個管理對象,然後在 layer 自動釋放並取消對 sprite 引用的時候,被立即釋放。從效果上來說,都是一幀之內完成的。但具體是哪種情況呢?我不知曉 : p 也不用知曉 ~ 所謂不知爲不知,是知也 ~

寫在後面

自動管理,所謂自動管理就是通過 create() 方法創建的對象(當然其內部是通過 autorelease() 方法標示,create 只是提供一個統一的創建對象方式),而什麼又是有用無用呢,文中我們看到 retain() 和 release(),而這就是有用無用的實現原理,使用就 retain ,移除使用就用 release,再細究內部,可知裏面維護了一個引用計數,從而判斷是否被使用 ,而前文我們知道 layer->addChild(obj),那麼 obj 就爲 layer 所用,究其本質,也是其內部調用了其 retain 等方法,可以閱讀官方相關文檔,有詳細的說明,而本文多是以抽象的概念解說其設計理念,從使用者的角度分析在使用過程中可能會出現的問題,因爲要想達到相同的自動管理效果,實現方式可以有很多種。別太注重細節,如果有什麼疑問,可以像這樣,通過幾個小例程去驗證我們的想法。對於本文,也只是我對 cocos2d-x 自動管理的理解,如果在實現和概念上有什麼說的不對,還請指出,畢竟是 淺說 ~

cocos2d-x 主要以 CCNode 爲基類的樹形結構組織管理,所以本文所創建的例程,基於 CCNode 編寫,當然內存的自動管理還有很多內容,比如緩存的實現,消息機制對象的生命週期等。但基於誰使用,誰處理的原則,思路倒也明晰 ~


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