cocos2dx[3.2](24)——內存管理機制

【嘮叨】

    整合參考文檔。


【參考】

    http://zh.wikipedia.org/wiki/引用計數 (引用計數——維基百科)

    http://cn.cocos2d-x.org/tutorial/show?id=2300 (引用計數和自動釋放池)

    http://cn.cocos2d-x.org/tutorial/show?id=1331 (內存管理——繞不過去的坎)

    http://blog.csdn.net/legendof1991/article/details/23360131 (內存優化)

    https://github.com/chukong/cocos-docs/blob/master/manual/framework/native/v3/memory-management/zh.md (內存管理機制)




【內存管理機制】

    在3.x版本,Cocos2d-x採用全新的根類 Ref ,實現Cocos2d-x 類對象的引用計數記錄。引擎中的所有類都派生自Ref。


1、引用計數

    引用計數的概念參考《維基百科》:http://zh.wikipedia.org/wiki/引用計數


    Cocos2d-x 提供引用計數管理內存。

        > 調用 retain() 方法             :令其引用計數增1,表示獲取該對象的引用權。

        > 調用 release() 方法          :在引用結束的時候,令其引用計數值減1,表示釋放該對象的引用權。

        > 調用 autorelease() 方法 :將對象放入自動釋放池。

        > 當釋放池自身被釋放的時候,它就會對池中的所有對象執行一次release()方法,實現靈活的垃圾回收。

    Cocos2d-x 提供 AutoreleasePool,管理自動釋放對象。

        > 當釋放池自身被釋放的時候,它就會對池中的所有對象執行一次release()方法。


    核心類Ref:實現了引用計數。

//
/**
 *	CCRef.h
 **/
	class CC_DLL Ref
	{
		public:
			void retain();      // 保留。引用計數+1
			void release();     // 釋放。引用計數-1
			Ref* autorelease(); // 實現自動釋放。
			unsigned int getReferenceCount() const; //被引用次數
		protected:
			Ref();          // 初始化
		public:
			virtual ~Ref(); // 析構
		protected:
			unsigned int _referenceCount; // 引用次數
			friend class AutoreleasePool; // 自動釋放池
	};


/**
 *	CCRef.cpp
 **/
	// 節點被創建時,引用次數爲 1
	Ref::Ref() : _referenceCount(1)
	{
	}

	void Ref::retain()
	{
		CCASSERT(_referenceCount > 0, "reference count should greater than 0");
		++_referenceCount;
	}

	void Ref::release()
	{
		CCASSERT(_referenceCount > 0, "reference count should greater than 0");
		--_referenceCount;
		if (_referenceCount == 0)
		{
			delete this;
		}
	}

	Ref* Ref::autorelease()
	{
		// 將節點加入自動釋放池
		PoolManager::getInstance()->getCurrentPool()->addObject(this);
		return this;
	}
//

    Ref原理分析:

        > 當一個 Ref 初始化(被new出來時),_referenceCount = 1;

        > 當調用該 Ref 的 retain() 方法時,_referenceCount++;

        > 當調用該 Ref 的 release() 方法時,_referenceCount--。

        > 若  _referenceCount 減後爲0,則 delete 該 Ref。


2、retain() 和 release() 使用

    下面一段簡單的例子來學習 retain() 和 release() 的使用。

//
	TestObject* obj1 = new TestObject("testobj1");
	CCLOG("obj1 referenceCount=%d",obj1->getReferenceCount());
	
	obj1->retain();
	CCLOG("obj1 referenceCount=%d",obj1->getReferenceCount());
	
	obj1->release();
	CCLOG("obj1 referenceCount=%d",obj1->getReferenceCount());
	
	obj1->release();
//

    控制檯顯示的日誌如下:

cocos2d: TestObject:testobj1 is created

cocos2d: obj1 referenceCount=1

cocos2d: obj1 referenceCount=2

cocos2d: obj1 referenceCount=1

cocos2d: TestObject:testobj1 is destroyed


    通過例子和打印結果可以看到:

        > obj1對象創建後,引用計數爲1;

        > 執行一次retain()後,引用計數爲2;

        > 執行一次release()後,引用計數回到1;

        > 再執行一次release()後,對象會被釋放掉。

    因此:

        > 我們可以調用retain()方法,令其引用計數增1,表示獲取該對象的引用權;

        > 在引用結束的時候調用release()方法,令其引用計數值減1,表示釋放該對象的引用權。

        > 直到對象的引用計數爲0,釋放該對象。


3、autorelease() 使用

    同樣一段簡單的例子來學習autorelease的使用,代碼如下:

//
	TestObject* obj = new TestObject("testobj");
	CCLOG("obj referenceCount=%d",obj->getReferenceCount());
	
	obj->autorelease();
	CCLOG("obj is add in currentpool %s",PoolManager::getInstance()->getCurrentPool()->contains(obj)?"true":"false");
	CCLOG("obj referenceCount=%d",obj->getReferenceCount());
	
	obj->retain();
	CCLOG("obj referenceCount=%d",obj->getReferenceCount());

	obj->release();
	CCLOG("obj referenceCount=%d",obj->getReferenceCount());

	//obj in current pool will be release
	Director::getInstance()->replaceScene(this);
//

    控制檯顯示的日誌如下:

cocos2d: TestObject:testobj is created

cocos2d: obj referenceCount=1

cocos2d: obj is add in currentpool true

cocos2d: obj referenceCount=1

cocos2d: obj referenceCount=2

cocos2d: obj referenceCount=1

...

cocos2d: TestObject:testobj is destroyed


    通過代碼和打印結果,我們可以看到:

        > obj對象創建後,引用計數爲1;

        > 執行一次autorelease()後,obj對象被加入到當前的自動釋放池。

        > obj對象的引用計數值並沒有減1。

        > 但是在下一幀開始前,當前的自動釋放池會被回收掉,並對自動釋放池中的所有對象執行一次release()操作。

        > 當對象的引用計數爲0時,對象會被釋放掉。


        > obj對象執行autorelease()後,我們對其執行了一組retain()和release()操作。

        > 此時obj對象的引用計數爲1,在場景切換後,當前的自動釋放池被回收,

        > obj對象執行一次release()操作引用計數減爲0時,對象會被釋放掉。


    注意:autorelease()只有在自動釋放池被釋放時纔會進行一次釋放操作,如果對象釋放的次數超過了應有的次數,則這個錯誤在調用autorelease()時並不會被發現,只有當自動釋放池被釋放時(通常也就是遊戲的每一幀結束時),遊戲纔會崩潰。在這種情況下,定位錯誤就變得十分困難了。

    例如,在遊戲中,一個對象含有1個引用計數,但是卻被調用了兩次autorelease()。在第二次調用autorelease()時,遊戲會繼續執行這一幀,結束遊戲時纔會崩潰,很難及時找到出錯的地點。

    因此,我們建議在開發過程中應該避免濫用autorelease(),只在工廠方法等不得不用的情況下使用,儘量以release()來釋放對象引用。


4、AutoreleasePool類 使用

    Cocos2d-x提供AutoreleasePool,管理自動釋放對象

    下面一段簡單的例子講解AutoreleasePool的使用,代碼如下:

//
	TestObject* obj2 = new TestObject("testobj2");
	CCLOG("obj2 referenceCount=%d",obj2->getReferenceCount());
	
	//use AutoreleasePool
	{
		AutoreleasePool pool;
	
		obj2->retain();
		CCLOG("obj2 referenceCount=%d",obj2->getReferenceCount());
	
		obj2->release();
		CCLOG("obj2 referenceCount=%d",obj2->getReferenceCount());
	
		obj2->autorelease();
		CCLOG("obj2 is add in pool %s",pool.contains(obj2)?"true":"false");
	
		TestObject *obj3 = new TestObject("testobj3");
		
		obj3->autorelease();
		CCLOG("obj3 is add in pool %s",pool.contains(obj3)?"true":"false");
	}
//

    控制檯輸出日誌如下:

cocos2d: TestObject:testobj2 is created

cocos2d: obj2 referenceCount=1

cocos2d: obj2 referenceCount=2

cocos2d: obj2 referenceCount=1

cocos2d: obj2 is add in pool true

cocos2d: TestObject:testobj3 is created

cocos2d: obj3 is add in pool true

cocos2d: TestObject:testobj2 is destroyed

cocos2d: TestObject:testobj3 is destroyed


    通過代碼和輸出結果,可以看到:

        > 創建了一個obj2對象,此時obj2對象的引用計數爲1。

        > 接着創建了一個自動釋放池,對obj2對象執行retain()和release()操作後,執行autorelease()操作,此時obj2對象被加入到當前新建的自動釋放池中。

        > 接着新建了obj3對象,並執行autorelease()操作。同樣obj3也被加入到當前新建的自動釋放池中。

        > 在代碼塊結束後,自動釋放池被回收,加入自動釋放池中的obj2和obj3執行release()操作,引用計數減爲0,被釋放銷燬。


    我們可以自己創建AutoreleasePool,管理對象的autorelease。

    我們已經知道,調用了autorelease()方法的對象(下面簡稱"autorelease對象"),將會在自動釋放池釋放的時候被釋放一次。雖然,Cocos2d-x已經保證每一幀結束後釋放一次釋放池,並在下一幀開始前創建一個新的釋放池,但是我們也應該考慮到釋放池本身維護着一個將要執行釋放操作的對象列表,如果在一幀之內生成了大量的autorelease對象,將會導致釋放池性能下降。因此,在生成autorelease對象密集的區域(通常是循環中)的前後,我們最好可以手動創建並釋放一個回收池。

    例如:

//
	// example of using temple autorelease pool
	{
		AutoreleasePool pool2;
		char name[20];
		for (int i = 0; i < 100; ++i)
		{
			snprintf(name, 20, "object%d", i);
			TestObject *tmpObj = new TestObject(name);
			tmpObj->autorelease();
		}
	}
//

    總結:

        > autorelease()的實質是將對象加入自動釋放池,對象的引用計數不會立刻減1,在自動釋放池被回收時對象執行release()。

        > autorelease()並不是毫無代價的,其背後的釋放池機制同樣需要佔用內存和CPU資源。

        > 過多的使用autorelease()會增加自動釋放池的管理和釋放池維護對象存取釋放的支出。

        > 在內存和CPU資源本就不足的程序中使得系統資源更加緊張。

        > 此時就需要我們合理創建自動釋放池管理對象autorelease。

        > 不用的對象推薦使用release()來釋放對象引用,立即回收。


5、特殊內存管理


  5.1、工廠方法 create()

    在Cocos2d-x中,提供了大量的工廠方法創建對象。仔細看你會發現,這些對象都是自動釋放的。

    下面以 Label 的 create 方法爲例,代碼如下:

//
	Label* Label::create()
	{
		auto ret = new Label();
		if (ret)
		{
			ret->autorelease();
		}
		return ret;
	}
//

    我們可以發現,創建了一個Label的對象,並對該對象執行autorelease()。表示該對象是自動釋放的。細心的你會發放Layer/Scene/Sprite等類的 create() 方法都相同。

    使用工廠方法創建對象時,雖然引用計數也爲1,但是由於對象已經被放入了釋放池,因此調用者沒有對該對象的引用權,除非我們人爲地調用了retain()來獲取引用權,否則,不需要主動釋放對象。


  5.2、Node 的 addChild() / removeChild 方法

    在Cocos2d-x中,所有繼承自Node類,在調用 addChild 方法添加子節點時,自動調用了retain。 對應的通過 removeChild,移除子節點時,自動調用了release。

    調用addChild方法添加子節點,節點對象執行retain。子節點被加入到節點容器中,父節點銷燬時,會銷燬節點容器釋放子節點。對子節點執行release。如果想提前移除子節點我們可以調用removeChild。

    在Cocos2d-x內存管理中,大部分情況下我們通過調用 addChild/removeChild 的方式自動完成了retain,release調用。不需再調用retain,release。




【內存優化】


1、內存優化原理

    爲優化應用內存使用,開發人員首先應該知什麼最耗應用內存,答案就是紋理! 紋理幾乎會佔據90%應用內存。所以儘量最小化應用的紋理內存使用,否則應用很有可能會因爲低內存而崩潰。

    本節介紹Cocos2d-x遊戲通用的兩條內存優化原理指導


  1.1、認識瓶頸尋找方案

    什麼樣的紋理最耗應用內存?或這些紋理會消耗多少內存?

    當然這個不用手動計算,只需猜測。工具在這裏已經準備好了,使用的是蘋果的工具“Allocation & Leaks”。你可以在Xcode中長按“Run”命令,選擇“ Profile ”來啓動這兩個工具。

    如下所示:

wKiom1TUx5TC2FpgAAD0F4fYCY4427.jpg


    使用Allocation工具可以監控應用的內存使用,使用Leaks工具可以觀察內存的泄漏情況。

    此外還可用一些代碼獲取遊戲內存使用的其他信息。

    如下所示:

//
	Sprite* bg = Sprite::create("HelloWorld.png");
	bg->setPosition(240, 160);
	this->addChild(bg);

	CCLOG("%s", Director::getInstance()->getTextureCache()->getCachedTextureInfo().c_str());
//

    調用這個代碼後,遊戲便會在DEBUG模式運行,這時你會在Xcode控制檯窗口看到一些格式工整的日誌信息。  

//
	cocos2d: "****/HelloWorld.png" rc=2 id=3 480 x 320 @ 32 bpp => 600 KB
	"/cc_fps_p_w_picpaths" rc=5 id=2 999 x 54 @ 16 bpp => 105 KB
	TextureCache dumpDebugInfo: 2 textures, for 705 KB (0.69 MB)
//

    從上可以看到會顯示紋理的名稱、引用計數、ID、大小及每像素的位數。最重要的是會顯示內存的使用情況。如“cc_fps_p_w_picpaths”指消耗了105KB內存,而“HelloWorld.png”消耗了600KB內存。


  1.2、切勿過度優化

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


2、內存優化水平

    在此將ccos2d-x內存優化分爲:三個等級

    每個等級都有不同的說明,策略也有點不一樣。


  2.1、客戶端等級

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

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

    第二: 音頻優化,3個因素會影響音頻文件的內存使用,即音頻文件數據格式、比特率及採樣率。推薦使用MP3數據格式的音頻文件,因爲Android平臺和iOS平臺均支持MP3格式,此外MP3格式經過壓縮和硬件加速。背景音樂文件大小應該低於800KB,最簡單的方法就是減少背景音樂時間然後重複播放。音頻文件採樣率大約在96-128kbps爲佳,比特率44kHz就夠了。

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


  2.2、引擎等級

    需要 OpenGL ES 及遊戲引擎高手。


  2.3、C++語言等級

    在這個等級中,建議是編寫無內存泄露代碼。遵循Cocos2d-x內置的內存管理原則,儘量避免內存泄露。


3.、提示和技巧

    (1)    一幀一幀載入遊戲資源

    (2)    減少繪製調用,使用“Auto-batching”自動批處理。

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

    (4)    避免高峯內存使用

    (5)    使用載入屏幕預載入遊戲資源

    (6)    需要時釋放空閒資源

    (7)    收到內存警告後釋放緩存資源.

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

    (9)    使用JPG格式要謹慎!

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

    (11)  請使用NPOT紋理,不要使用POT紋理

    (12)  避免載入超大紋理

    (13)  推薦1024*1024 NPOT pvr.ccz紋理集,而不要採用RAW PNG紋理



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