cocos2d-x中的設(shè)計(jì)模式

本文由子龍山人原創(chuàng),原文鏈接:http://www.zilongshanren.com/cocos2d-x-design-pattern-singleton1/


cocos2d-x 官網(wǎng): http://www.cocos.com/docs/html5/v3/singleton-objs/zh.html

1.Cocos2D-x中的單例如下:


CCDirector ,CCTextureCache, CCSpriteFrameCache, CCAnimationCache, CCUserDefault, CCNotificationCenter, CCShaderCache, CCScriptEngineManager, CCFileUtils, SimleAudioEngie

為什么會存在這樣一些單例呢?

CCDirector單例:

它負(fù)責(zé)管理初始化OpenGL渲染窗口以及游戲場景的流程控制,它是cocos2dx游戲開發(fā)中必不可少的類之一。

為什么要把此類設(shè)計(jì)成單例對象呢?
因?yàn)椋粋€(gè)游戲只需要有一個(gè)游戲窗口就夠了,所以,只需要初始化一次OpenGL渲染窗口。而且場景的流程控制功能,也只需要存在一個(gè)這樣的場景控制對象即可。為了保證CCDirector類只存在一個(gè)實(shí)例對象,就必須使用單例模式。

CCTextureCache單例:

此類主要負(fù)責(zé)加載游戲當(dāng)中所需要的紋理圖片資源,這些資源加載好以后,就可以一直保留在內(nèi)存里面,當(dāng)下次再需要使用此紋理的時(shí)候,直接返回相應(yīng)的紋理對象引用就可以了,無需再重復(fù)加載。當(dāng)然,這樣做可能會很浪費(fèi)內(nèi)存,所以cocos2dx采用了一種引用計(jì)數(shù)的方式來管理對象內(nèi)存,當(dāng)紋理剛被加載進(jìn)來的時(shí)候,引用計(jì)數(shù)為1。如果使用此紋理對象創(chuàng)建一個(gè)精靈,那么此紋理對象引用會加1.如果精靈被釋放,則相應(yīng)的引用計(jì)數(shù)減1.當(dāng)紋理的引用計(jì)數(shù)變?yōu)?的時(shí)候,紋理所占用的內(nèi)存自然就會被釋放掉。這也是為什么在收到內(nèi)存警告的時(shí)候,會調(diào)用CCTextureCache的removeUnusedTextures方法。此方法會將所有引用計(jì)數(shù)為1的紋理對象全部釋放掉。單從字面上看,Cache,即緩存的意思。它以犧牲一定的內(nèi)存壓力為代價(jià),帶來的是游戲性能的提升。這種cache技術(shù),在游戲開發(fā)中比比皆是。注:IO操作對游戲性能影響非常大,要極力避免!!!

擴(kuò)展:

類似的CCSpriteFrameCache、CCAnimationCache和CCShaderCache,它們也都是緩存類,分別負(fù)責(zé)緩存SpriteFrame、Animation和Shader。這樣做的原因無非就是為了性能,以空間換時(shí)間.


CCUserDefault單例:

此類主要是用來保存游戲中的數(shù)據(jù)用的,它會創(chuàng)建一個(gè)xml文件,并把用戶自定義的數(shù)據(jù)以key-value的形式存儲到此xml文件中。此類為什么會變成單例類呢?原因也很簡單,因?yàn)轭愃七@種操作數(shù)據(jù)文件,或者配置文件的類,通常只需要在程序運(yùn)行過程中存在一個(gè)實(shí)例即可。

CCNotificationCenter單例:

這是一個(gè)通知中心,它其實(shí)還運(yùn)用了一個(gè)觀察者模式,這里暫時(shí)不討論。該通知中心理論上也是只需要一個(gè)就夠了。但是,cocos2d-x在實(shí)現(xiàn)此單例的時(shí)候,并沒有將此類的構(gòu)造函數(shù)私有么,我在猜想,是不是開發(fā)人員有意為之呢?或者多個(gè)通知中心也有其存在的價(jià)值。這個(gè)大家可以討論一下。

CCScriptEngineManager單例:

此類包含一個(gè)實(shí)現(xiàn)了CCScriptEngineProtocl接口的對象引用,它可以幫助我們方便地找到LuaEngine對象。這里單例的作用純粹變成了LuaEngine的一個(gè)全局訪問點(diǎn)了。

為什么不直接把LuaEngine作為單例對象呢?

是否在某些情況之下,可能需要創(chuàng)建多個(gè)LuaEngine對象?如果考慮到cocos2d-x還可以同時(shí)支持其它的腳本引擎,那也可以相應(yīng)的把另外的腳本引擎設(shè)計(jì)成單例類。當(dāng)然,這樣做無疑會使引擎里面的單例過多。考慮到單例模式近年來被廣大開發(fā)者所詬病,已將其列入“反模式”。這里引用CCScriptEngineManager單例類,給其它引擎對象提供訪問的惟一全局點(diǎn),這也不失為一個(gè)辦法。不知我的推測是否正確?

CCFileUtils類:

該類是一個(gè)工具類。工具類和配置文件類,它們絕大多數(shù)情況也都是設(shè)計(jì)成單例的。因?yàn)樗鼈儧]有存在多個(gè)實(shí)例的必要。同時(shí),它們也可以實(shí)現(xiàn)為一組類方法,這樣無需創(chuàng)建對象也能夠使用。

SimpleAudioEngine類:

它也被設(shè)計(jì)成了一個(gè)單例類。因?yàn)樗峁┙o了開發(fā)人員最簡單的聲音操作接口,可以方便地處理游戲中的背景音樂和音效。此類同時(shí)還應(yīng)用了外觀模式,把CocoDenshion子系統(tǒng)中的復(fù)雜功能給屏蔽起來了,簡化了客戶端程序員的調(diào)用。該類為什么要設(shè)計(jì)成單例,是因?yàn)榈教幎家L問它。設(shè)計(jì)成單例會很方便,而且它與其它對象沒有什么聯(lián)系,不好使用對象組合。


2.使用單例模式的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):簡單易用,限制一個(gè)類只有一個(gè)實(shí)例,可以減少創(chuàng)建多個(gè)對象可能會引起的內(nèi)存問題的風(fēng)險(xiǎn),包括內(nèi)存泄漏、內(nèi)存占用問題。

缺點(diǎn):單例模式因?yàn)樘峁┝艘粋€(gè)全局的訪問點(diǎn),你可以在程序的任何地方輕而易取地訪問到,這本身就是一種高耦合的設(shè)計(jì)。一旦單例改變以后,其它模板都需要修改。另外,單例模式使得對象變成了全局的了。學(xué)過面對對象編程的人都知道,全局變量是非常邪惡的,要盡量不要使用。而且單例模式會使得對象的內(nèi)存在程序結(jié)束之前一直存在,在一些使用GC的語言里面,這其實(shí)就是一種內(nèi)存泄漏,因?yàn)樗鼈冇肋h(yuǎn)都不到釋放。當(dāng)然,也可以通過提供一些方法來釋放單例對象所占用的內(nèi)存,比如前面提到的XXXCache對象,都有相應(yīng)的Purge方法。最后,cocos2dx里面實(shí)現(xiàn)的單例,99%都不是線程安全的。

在討論優(yōu)缺點(diǎn)的時(shí)候,讀者想必也看出來了,缺點(diǎn)比優(yōu)點(diǎn)多多了。這是給大家提個(gè)醒,以后使用單例模式的時(shí)候要謹(jǐn)慎,不要濫用。因?yàn)榇四J阶钊菀妆粸E用。只有真正符合單例模式應(yīng)用場景的時(shí)候,才能考慮。不要為了訪問方便,就把任何類都弄成單例,這樣,到最后,你會發(fā)現(xiàn)你的程序里面就只剩下一堆單例和工廠了。此外,單例模式正在消減,比如CCActionManager和CCTouchDispatcher在cocos2d1.0之前也是單例,現(xiàn)在變成了CCDirector類的屬性了。而且Riq(cocos2d-iphone的作者)也有在郵件中提到,以后CCDirector對象也會變成非單例,并且允許一個(gè)游戲中創(chuàng)建多個(gè)游戲窗口。


3.單例模式的定義

public class Singleton
{
public:
//全局訪問點(diǎn)
static Singleton* SharedSingleton()
{
if(NULL == m_spSingleton)
{
m_spSingleton = new Singleton();
}
return m_spSingleton;
}
private:
static Singleton* m_spSingleton;
Singleton();
Singleton(const Singleton& other);
Singleton& operator=(const Singleton& other);
};
Singleton* Singleton::m_spSingleton = NULL;

注意,這里只是最基本的實(shí)現(xiàn),它沒有考慮到線程安全,也沒有考慮內(nèi)存釋放。但是,這個(gè)實(shí)現(xiàn)有兩個(gè)最基本的要素。一:定義一個(gè)靜態(tài)變量,并把構(gòu)造函數(shù)等設(shè)置為私有的。二:提供一個(gè)全局的訪問點(diǎn)給外部訪問。

4.游戲開發(fā)中如何運(yùn)用此模式呢?

眾所周知,游戲開發(fā)中離不開游戲數(shù)據(jù)保存和加載。這些數(shù)據(jù)包括關(guān)卡數(shù)據(jù)、游戲進(jìn)行中的狀態(tài)數(shù)據(jù)等。這樣一些信息很多游戲模塊中都需要訪問,所以可以為之設(shè)置一個(gè)單例對象。我武斷地認(rèn)為,客戶端游戲開發(fā)中,至少需要一個(gè)單例對象。因?yàn)橐粋€(gè)全局的訪問點(diǎn)可以方便很多對象之間的交互。根據(jù)之前的討論,也可以把一些時(shí)覺需要用到的類引用保存在此單例對象中,不過只需要保存弱引用即可。使用單例,最嚴(yán)重的就是怕內(nèi)存泄漏,所以,大家盡量不要把單例類設(shè)計(jì)地太復(fù)雜,也不要讓它包含過多的動態(tài)內(nèi)存管理工作。


二段構(gòu)建模式

所謂二段構(gòu)建,就是指創(chuàng)建對象時(shí)不是直接通過構(gòu)建函數(shù)來分配內(nèi)存并完成初始化操作。取而代之的是,構(gòu)造函數(shù)只負(fù)責(zé)分配內(nèi)存,而初始化的工作則由一些名為initXXX的成員方法來完成。然后再定義一些靜態(tài)類方法把這兩個(gè)階段組合起來,完成最終對象的構(gòu)建。因?yàn)樵凇禖ocoa設(shè)計(jì)模式》一書中,把此慣用法稱之為“Two Stage Creation”,即“二段構(gòu)建”。因?yàn)榇四J皆赾ocos2d里面被廣泛使用,所以把該模式也引入過來了。

1.應(yīng)用場景:

二段構(gòu)建在cocos2d-x里面隨處可見,自從2.0版本以后,所有的二段構(gòu)建方法的簽名都改成create了。這樣做的好處是一方面統(tǒng)一接口,方便記憶,另一方面是以前的類似Cocoa的命名規(guī)范不適用c++,容易引起歧義。下面以CCSprite為類,來具體闡述二段構(gòu)建的過程,請看下列代碼:

//此方法現(xiàn)在已經(jīng)不推薦使用了,將來可能會刪除
CCSprite* CCSprite::spriteWithFile(const char pszFileName)
{
return CCSprite::create(pszFileName);
}
CCSprite
CCSprite::create(const char *pszFileName)
{
CCSprite *pobSprite = new CCSprite(); //1.第一階段,分配內(nèi)存
if (pobSprite && pobSprite->initWithFile(pszFileName)) //2.第二階段,初始化
{
pobSprite->autorelease(); //!!!額外做了內(nèi)存管理的工作。
return pobSprite;
}
CC_SAFE_DELETE(pobSprite);

return NULL;

}

如上面代碼中的注釋所示,創(chuàng)建一個(gè)sprite明顯被分為兩個(gè)步驟:1.使用new來創(chuàng)建內(nèi)存;2.使用initXXX方法來完成初始化。
因?yàn)镃CSprite的構(gòu)造函數(shù)也有初始化的功能,所以,我們再來看看CCSprite的構(gòu)建函數(shù)實(shí)現(xiàn):

CCSprite::CCSprite(void)
: m_pobTexture(NULL)
, m_bShouldBeHidden(false)
{
}
很明顯,這個(gè)構(gòu)建函數(shù)所做的初始化工作非常有限,僅僅是在初始化列表里面初始化了m_pobTexture和m_bShouldBeHidden兩個(gè)變量。實(shí)際的初始化工作大部分都放在initXXX系列方法中,大家可以動手去查看源代碼。

2.分析為什么要使用此模式?

這種二段構(gòu)建對于C++程序員來說,其實(shí)有點(diǎn)別扭。因?yàn)閏++的構(gòu)造函數(shù)在設(shè)計(jì)之初就是用來分配內(nèi)存+初始化對象的。如果再搞個(gè)二段構(gòu)建,實(shí)則是多此一舉。但是,在objective-c里面是沒有構(gòu)造函數(shù)這一說的,所以,在Cocoa的編程世界里,二段構(gòu)建被廣泛采用。而cocos2d-x當(dāng)初是從cocos2d-iphone移植過來了,為了保持最大限度的代碼一致性,所以保留了這種二段構(gòu)建方式。這樣可以方便移植cocos2d-iphone的游戲,同時(shí)也方便cocos2d-iphone的程序員快速上手cocos2d-x。不過在后來,由于c++天生不具備oc那種可以指定每一個(gè)參數(shù)的名稱的能力,所以,cocos2d-x的設(shè)計(jì)者決定使用c++的函數(shù)重載來解決這個(gè)問題。這也是后來為什么2.0版本以后,都使用create函數(shù)的重載版本了.

雖然接口簽名改掉了,但是本質(zhì)并沒有變化,還是使用的二段構(gòu)建。二段構(gòu)建并沒有什么不好,只是更加突出了對象需要初始化。在某種程度上也可以說是一種設(shè)計(jì)強(qiáng)化。因?yàn)橥洺跏蓟且磺心涿畹腷ug的罪魁禍?zhǔn)住M瑫r(shí),二段構(gòu)建出來的對象都是autorelease的對象,而autorelease對象是使用引用計(jì)數(shù)來管理內(nèi)存的。客戶端程序員在使用此接口創(chuàng)建對象的時(shí)候,無需關(guān)心具體實(shí)現(xiàn)細(xì)節(jié),只要知道使用create方法可以創(chuàng)建并初始化一個(gè)自動釋放內(nèi)存的對象即可。

在一點(diǎn),在《Effective Java》一書中,也有提到。為每一個(gè)類提供一個(gè)靜態(tài)工廠方法來代替構(gòu)造函數(shù),

它有以下三個(gè)優(yōu)點(diǎn):

1.與構(gòu)造函數(shù)不同,靜態(tài)方法有名字,而構(gòu)造函數(shù)只能通過參數(shù)重載。
2.它每次被調(diào)用的時(shí)候,不一定都創(chuàng)建一個(gè)新的對象。比如Boolean.valueOf(boolean)。
3.它還可以返回原類型的子類型對象。
因此,使用二段構(gòu)建的原因有二:
1.兼容性、歷史遺留原因。(這也再次印證了一句話,一切系統(tǒng)都是遺留系統(tǒng),呵呵)
2.二段構(gòu)建有其自身獨(dú)有的優(yōu)勢。
3.使用此模式的優(yōu)缺點(diǎn)是什么?
優(yōu)點(diǎn):
1.顯示分開內(nèi)存分配和初始化階段,讓初始化地位突出。因?yàn)槌绦騿T一般不會忘記分配內(nèi)存,但卻常常忽略初始化的作用。
2.見上面分析《Effective Java》的第1條:“為每一個(gè)類提供一個(gè)靜態(tài)工廠方法來代替構(gòu)造函數(shù)”
3.除了完成對象構(gòu)建,還可以管理對象內(nèi)存。
缺點(diǎn):
1.不如直接使用構(gòu)造函數(shù)來得直白、明了,違反直覺,但這個(gè)是相對的。
4.此模式的定義及一般實(shí)現(xiàn)定義:將一個(gè)對象的構(gòu)建分為兩個(gè)步驟來進(jìn)行:1.分配內(nèi)存 2.初始化它的一般實(shí)現(xiàn)如下:

class Test
{
public:
//靜態(tài)工廠方法
static Test* create()
{
Test *pTest = new Test;
if (pTest && pTest->init()) {
//這里還可以做其它操作,比如cocos2d-x里面管理內(nèi)存
return pTest;
}
return NULL;
}
//
Test()
{
//分配成員變量的內(nèi)存,但不初始化
}
bool init(){
//這里初始化對象成員
return true;
}
private:
//這里定義數(shù)據(jù)成員
};

5.在游戲開發(fā)中如何運(yùn)用此模式

5.在游戲開發(fā)中如何運(yùn)用此模式這個(gè)也非常簡單,就是今后在使用cocos2d-x的時(shí)候,如果你繼承CCSprite實(shí)現(xiàn)自定義的精靈,你也需要按照“二段構(gòu)建”的方式,為你的類提供一個(gè)靜態(tài)工廠方法,同時(shí)編寫相應(yīng)的初始化方法。當(dāng)然,命名規(guī)范最好和cocos2d-x統(tǒng)一,即靜態(tài)工廠方法為create,而初始化方法為initXXXX。

6.此模式經(jīng)常與哪些模式配合使用

由于此模式在GoF的設(shè)計(jì)模式中并未出現(xiàn),所以暫時(shí)不討論與其它模式的關(guān)系。最后看看cocos2d-x創(chuàng)始人王哲對于為什么要設(shè)計(jì)成二段構(gòu)建的看法:“其實(shí)我們設(shè)計(jì)二段構(gòu)造時(shí)首先考慮其優(yōu)勢而非兼容cocos2d-iphone. 初始化時(shí)會遇到圖片資源不存在等異常,而C++構(gòu)造函數(shù)無返回值,只能用try-catch來處理異常,啟用try-catch會使編譯后二進(jìn)制文件大不少,故需要init返回bool值。Symbian, Bada SDK,objc的alloc + init也都是二階段構(gòu)造”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容