1. 程式人生 > >Cocos2dx記憶體管理、優化減少記憶體和多執行緒方面的問題

Cocos2dx記憶體管理、優化減少記憶體和多執行緒方面的問題

Cocos2d-x中的引用計數(Reference Count)和自動釋放池(AutoReleasePool)

引用計數

引用計數是c/c++專案中一種古老的記憶體管理方式。當我8年前在研究一款名叫TCPMP的開源專案的時候,引用計數就已經有了。

iOS SDK把這項計數封裝到了NSAutoreleasePool中。所以我們也在Cocos2d-x中克隆了一套CCAutoreleasePool。兩者的用法基本上一樣,所以假如你沒有涉及過ios開發,你可以看看蘋果官方文件NSAutoreleasePool Class Reference。

CCAutoreleasePool

Cocos2d-x的CCAutoreleasePool和cocoa的NSAutoreleasePool有相同的概念和API,但是有兩點比較重要的不同:

1.    CCAutoreleasePool不能被開發者自己建立。Cocos2d-x會為我們每一個遊戲建立一個自動釋放池例項物件,遊戲開發者不能新建自動釋放池,僅僅需要專注於release/retain cocos2d::CCObject的物件。

2.    CCAutoreleasePool不能被用在多執行緒中,所以假如你遊戲需要網路執行緒,請僅僅在網路執行緒中接收資料,改變狀態標誌,不要這個執行緒裡面呼叫cocos2d介面。下面就是原因:

CCAutoreleasePool的邏輯是,當你呼叫object->autorelease(),object就被放到自動釋放池中。自動釋放池能夠幫助你保持這個object的生命週期,直到當前訊息迴圈的結束。在這個訊息迴圈的最後,假如這個object沒有被其他類或容器retain過,那麼它將自動釋放掉。例如,layer->addChild(sprite),這個sprite增加到這個layer的子節點列表中,他的宣告週期就會持續到這個layer釋放的時候,而不會在當前訊息迴圈的最後被釋放掉。這就是為什麼你不能在網路線層中管理CCObject生命週期,因為在每一個UI執行緒的最後,自動釋放物件將會被刪除,所以當你呼叫這些被刪掉的物件的時候,你就會遇到crash。

CCObject::release(), retain() and autorelease() 

簡而言之,這隻有兩種情況你需要呼叫release()方法

1.    你new一個cocos2d::CCObject子類的物件,例如CCSprite,CCLayer等。

2.    你得到cocos2d::CCObject子類物件的指標,然後在你的程式碼中呼叫過retain方法。

下面例子就是不需要呼叫retain和release方法:

CCSprite* sprite = CCSprite::create("player.png"); 

這裡就沒有更多的程式碼用於sprite了。但是請注意sripte->autorelease()已經在CCSprite::create(const char*)方法中被呼叫了,因此這個sprite將在訊息迴圈的最後自動釋放掉。

一個錯誤的例子

一個開發者報告了一個使用CCArray 並導致crash的例子

bool HelloWorld::init()
{
    bool bRet = false;
    do
    {
        //////////////////////////////////////////////////////////////////////////
        // super init first
        //////////////////////////////////////////////////////////////////////////
        CC_BREAK_IF(! CCLayer::init());
        //////////////////////////////////////////////////////////////////////////
        // add your codes below...
        //////////////////////////////////////////////////////////////////////////
        CCSprite* bomb1 = CCSprite::create("CloseNormal.png");
        CCSprite* bomb2 = CCSprite::create("CloseNormal.png");
        CCSprite* bomb3 = CCSprite::create("CloseNormal.png");
        CCSprite* bomb4 = CCSprite::create("CloseNormal.png");
        CCSprite* bomb5 = CCSprite::create("CloseNormal.png");
        CCSprite* bomb6 = CCSprite::create("CloseNormal.png");
        addChild(bomb1,1);
        addChild(bomb2,1);
        addChild(bomb3,1);
        addChild(bomb4,1);
        addChild(bomb5,1);
        addChild(bomb6,1);
        m_pBombsDisplayed = CCArray::create(bomb1,bomb2,bomb3,bomb4,bomb5,bomb6,NULL);
        //m_pBombsDisplayed 是在標頭檔案中被定義為一個 protected 變數.
        // <--- 我們應該新增在這裡m_pBombsDisplayed->retain()方法來防止在HelloWorld::refreshData()中crash。
        this->scheduleUpdate();
        bRet = true;
    } while (0);
    return bRet;
}
void HelloWorld::update(ccTime dt)
{
    refreshData();
}
void HelloWorld::refreshData()
{
    m_pBombsDisplayed->objectAtIndex(0)->setPosition(cpp(100,100));
}

他的錯誤是m_pBombsDisplayed是使用CCArray::create(…)建立的,這種建立方式是靜態構造方式,這個陣列被標記了autorelease。

所以這個陣列會在當前訊息迴圈的最後被CCAutoreleasePool釋放掉。

當後面的訊息迴圈呼叫HelloWorld::update(ccTime)的時候,m_pBombsDisplayed已經是一個野指標了,這就將引起崩潰。

為了修復這個崩潰情況,我們需要增加m_pBombsDisplayed->retain()在m_pBombsDisplayed =CCArray::create(…);之後,並且在 HelloWorld::~HelloWorld() 的解構函式中呼叫m_pBombsDisplayed->release()。

紋理快取(TextureCache)

簡介

紋理快取是將紋理快取起來方便之後的繪製工作。每一個快取的影象的大小,顏色和區域範圍都是可以被修改的。這些資訊都是儲存在記憶體中的,不用在每一次繪製的時候都發送給GPU。

CCTextureCache

Cocos2d通過呼叫CCTextureCache或者CCSpriteFrameCache來快取精靈的紋理。

當這個精靈呼叫CCTextureCache 或 CCSpriteFrameCache的方法的時候,cocos2dx將使用紋理快取來建立一個CCSprite。所以你可以預先將紋理載入到快取中,這樣你在場景中使用的時候就非常方便了。怎麼樣載入這些紋理就看你自己的想法。例如,你可以選擇非同步載入方式,這樣你就可以為loading場景增加一個進度條。

當你建立一個精靈,你一般會使用CCSprite::create(pszFileName)。假如你去看CCSprite::create(pszFileName)的實現方式,你將看到它將這個圖片增加到紋理快取中去了:

bool CCSprite::initWithFile(const char *pszFilename)
{
    CCAssert(pszFilename != NULL, "Invalid filename for sprite");
    CCTexture2D *pTexture = CCTextureCache::sharedTextureCache()->addImage(pszFilename);
    if (pTexture)
    {
        CCRect rect = CCRectZero;
        rect.size = pTexture->getContentSize();
        return initWithTexture(pTexture, rect);
    }
    // don't release here.
    // when load texture failed, it's better to get a "transparent" sprite than a crashed program
    // this->release(); 
    returnfalse;
}

上面程式碼顯示一個單例在控制載入紋理。一旦這個紋理被載入了,在下一時刻就會返回之前載入的紋理引用,並且減少載入的時候瞬間增加的記憶體。(詳細API請看CCTextureCache API)

使用快取的原因就是減少記憶體,因為當你使用一個圖片建立一個精靈的時候,如果這個圖片不在快取中,那麼就會將他載入到快取中,當你需要用相同的圖片來新建精靈的時候,就可以直接從快取中取得,而不用再去新分配一份記憶體空間。

值得注意的是清理的順序,應該先清理動畫快取,然後清理精靈幀,最後是紋理。按照引用層級由高到低,以保證保釋引用有效。

引擎會在裝置出現記憶體警告時自動清理快取,但是這顯然在很多情況下已經為時已晚了。一般情況下,我們應該在切換場景時清理快取中的無用紋理,因為不同場景間使用的紋理不同的。如果確實存在著共享的紋理,將其加入一個標記陣列來保持其引用計數,以避免被清理。

void removeAnimationByName(const char *name); //移除一個指定的動畫

實際上,如果考慮到兩個場景間使用的動畫基本不會重複,可以直接清理整個動畫快取。

void removeUnusedSpriteFrames(); //清理無用快取

void removeUnusedTextures();  //清除不使用的紋理

如何優化記憶體使用

記憶體優化原理

為優化應用記憶體使用,開發人員首先應該知道什麼最耗應用記憶體,答案就是紋理!紋理幾乎會佔據90%應用記憶體。所以儘量最小化應用的紋理記憶體使用,否則應用很有可能會因為低記憶體而崩潰。本文介紹Cocos2d-x遊戲通用的兩條記憶體優化原理指導。

切勿過度優化

這是一個通用的優化規則。在優化過程中,應該做一些權衡取捨。因為有時候影象質量和影象記憶體使用是處於兩級的狀態。千萬不要過度優化!

記憶體優化水平

在此將ccos2d-x記憶體優化分為三個等級。每個等級都有不同的說明,策略也有點不一樣。

客戶端等級

這是最重要的的優化等級。因為我們要在Cocos2d-x引擎頂層編譯遊戲,引擎自身會提供一些優化選項。在這個等級我們可以進行大部分優化。簡而言之,我們可以優化紋理、音訊、字型及粒子的記憶體使用。

·        首先看紋理優化,為了優化紋理記憶體使用,必須知道什麼因素對紋理記憶體使用的影響最大。主要有3個因素會影響紋理記憶體,即紋理格式(壓縮還是非壓縮)、顏色深度和大小。我們可以使用PVR格式紋理減少記憶體使用。推薦紋理格式為pvr.ccz。紋理使用的每種顏色位數越多,影象質量越好,但是越耗記憶體。所以我們可以使用顏色深度為RGB4444的紋理代替RGB8888,這樣記憶體消耗會降低一半。此外超大的紋理也會導致記憶體相關問題。所以最好使用中等大小的紋理。

·        音訊優化,3個因素會影響音訊檔案的記憶體使用,即音訊檔案資料格式、位元率及取樣率。推薦使用MP3資料格式的音訊檔案,因為Android平臺和iOS平臺均支援MP3格式,此外MP3格式經過壓縮和硬體加速。背景音樂檔案大小應該低於800KB,最簡單的方法就是減少背景音樂時間然後重複播放。音訊檔案取樣率大約在96-128kbps為佳,位元率44kHz就夠了。

·        字型和粒子優化,在此有兩條小提示:使用BMFont字型顯示遊戲分數時,請儘可能使用最少數量的文字。例如只想要顯示單位數的數字,你可以移除所有字母。至於粒子,可以通過減少粒子數來降低記憶體使用。

引擎等級

如果你不是一個OpenGLES及遊戲引擎高手,可以略過這部分。因為Cocos2d-x是一個開源遊戲引擎,如果你在引擎等級中做了什麼優化,請告知我們!歡迎任何改進和程式碼。

C++語言等級

在這個等級中,建議是編寫無記憶體洩露程式碼。遵循Cocos2d-x內建的記憶體管理原則,儘量避免記憶體洩露。

提示和技巧

1.    一幀一幀載入遊戲資源

2.    減少繪製呼叫,使用“CCSpriteBatchNode”

3.    載入紋理時按照從大到小的順序

4.    避免高峰記憶體使用

5.    使用載入螢幕預載入遊戲資源

6.    需要時釋放空閒資源

7.    收到記憶體警告後釋放快取資源.

8.    使用紋理打包器優化紋理大小、格式、顏色深度等

9.    使用JPG格式要謹慎!

10.  請使用RGB4444顏色深度16位紋理

11.  請使用NPOT紋理,不要使用POT紋理

12.  避免載入超大紋理

13.  推薦1024*1024 NPOT pvr.ccz紋理集,而不要採用RAWPNG紋理

推薦閱讀

cocos2dx裡面,sprite本身不消耗多少記憶體,只是關聯的材質檔案消耗記憶體。假設有10個sprite關聯同一個材質,也不會有10倍消耗。

關於圖片佔用的材質記憶體,我覺得還有好幾種優化手段:
1、對於背景圖,因為不需要考慮透明問題。載入材質時可以使用 RGB565 格式(5位紅色,6位綠色,5位藍色),每一個畫素消耗的記憶體是16bit = 2byte。比預設的 RGBA8888 消耗的記憶體少一半。
2、大尺寸的圖可以適當縮小,顯示時拉伸放大。比如960x640的圖可以縮小為768x512,消耗的記憶體減少一半。
3、有些sprite不需要那麼多的色彩,可以用 RGBA4444 格式載入,一個畫素也只消耗2byte,減少一半。可以用 TexturePacker 這樣的工具處理原始 32bitpng 圖片,生成 RGBA4444 格式的材質檔案。
4、多個小圖合併到一起,做成 sprite sheet,可以顯著降低記憶體使用,效能也會好一點。
5、超大背景圖裁剪成多個小塊,需要顯示哪個區域才載入對應的塊。程式上覆雜不少,但總比記憶體不足崩潰掉好。

以上優化方法,我個人實踐下來效果還是很明顯的。

1。OpenGL一般都用2的N次方貼圖尺寸,所以製作貼圖時使它儘量接近2的N次方值,比如128*128。如果有個切片組成的貼圖尺寸是130*100,那就嘗試重新排布切片,使它控制在128*128以內,這樣能減少一半記憶體。
2。可以使用小圖放大來貼,但我建議是對於背景圖這樣,對清晰度要求不高時,將尺寸按整數倍縮小貼圖,然後貼圖時採用Nearest臨近差值演算法,這樣效果可以接受,效率又高。
3。將切片合成貼圖時,要注意將使用時機和頻率相當的切片放在一起,這樣可以做到當前載入進記憶體的貼圖,都是使用頻率最高的。而當前不用的貼圖要及時釋放掉。遊戲玩家一般都可以接受等待進度條載入一些東西。
4。遊戲前期可以用模擬器測試,後期一定要真機+Instruments測試,Xcode的Instruments功能很強,可以檢測Leak,OpenGL優化建議,Allocation等。
5。遊戲注意避免在logic迴圈或者render迴圈中反覆分配記憶體,造成惡性增長。
6。PNG要用PS針對Web壓縮一下,可以縮小檔案尺寸,雖然不會減少執行時記憶體分配,但可以減少貼圖載入的IO時間


記憶體管理

cocos2d::Vector<T>類只包含一個成員資料:

std::vector<T> _data;

_data的記憶體管理是由編譯器自動處理的,如果聲明瞭一個cocos2d::Vector<T>型別,就不必費心去釋放記憶體。 注意:使用現代的c++,本地儲存物件比堆儲存物件好。所以請不要用new操作來申請cocos2d::Vector<T>的堆物件,請使用棧物件。如果真心想動態分配堆cocos2d::Vector<T>,請將原始指標用智慧指標來覆蓋。 警告:cocos2d::Vector<T>並不是cocos2d::Object的子類,所以不要像使用其他cocos2d類一樣來用retain/release和引用計數記憶體管理。

最佳做法

·        考慮基於棧的cocos2d::Vector<T>優先用於基於堆的

·        當將cocos2d::Vector<T>作為引數傳遞時,將它宣告成常量引用:constcocos2d::Vector<T>&

·        返回值是cocos2d::Vector<T>時,直接返回值,這種情況下編譯器會優化成移動操作。

·        不要用任何沒有繼承cocos2d::Object的型別作為cocos2d::Vector<T>的資料型別。

cocos2d::Map<K,V>類只包含一個數據成員:

typedef std::unordered_map<K, V> RefMap;
RefMap _data;

_data的記憶體管理是由編譯器處理的,當在棧中宣告cocos2d::Map<K,V>物件時,無需費心釋放它佔用的記憶體。但是如果你是使用new操作來動態分配cocos2d::Map<K,V>的記憶體的話,就得用delete來釋放記憶體了,new[]操作也一樣。

注意:使用現代的c++,本地儲存物件比堆儲存物件好。所以請不要用new操作來分配cocos2d::Map<K,V>的堆物件,請使用棧物件。

如果真心想動態分配堆cocos2d::Map<K,V>,請將原始指標用智慧指標來覆蓋。

警告:cocos2d::Map<K,V>並不是cocos2d::Object的子類,所以不要像使用其他cocos2d類一樣來用retain/release和引用計數記憶體管理。

警告:cocos2d::Map<K,V>並沒有過載[]操作,不要用下標[i]來取cocos2d::Map<K,V>物件中的元素。

記憶體管理

cocos2d::Value的記憶體由它的解構函式來釋放,所以使用cocos2d::Value時請儘量用推薦的最佳做法。

cocos2d::Value包含下面的資料成員:

union
{
    unsigned char byteVal;
    int intVal;
    float floatVal;
    double doubleVal;
    bool boolVal;
}_baseData;
std::string _strData;
ValueVector* _vectorData;
ValueMap* _mapData;
ValueMapIntKey* _intKeyMapData;
Type _type;

程式碼段中,_baseData_strData_type是由編譯器和它們的解構函式負責釋放記憶體的,而cocos2d::Value的解構函式則負責釋放指標成員(_vectorData_mapDataintKeyMapData)。

注意:cocos2d::Value不能像其它cocos2d型別一樣使用retain/release和refcount記憶體管理

CCArray繼承至CCObject(CCObject主要是為了自動記憶體管理而建立的),並且提供了一系列介面