本文由子龍山人原創(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
為什么會(huì)存在這樣一些單例呢德崭?
CCDirector單例:
它負(fù)責(zé)管理初始化OpenGL渲染窗口以及游戲場景的流程控制阶捆,它是cocos2dx游戲開發(fā)中必不可少的類之一双肤。
為什么要把此類設(shè)計(jì)成單例對(duì)象呢杉适?
因?yàn)橄夷簦粋€(gè)游戲只需要有一個(gè)游戲窗口就夠了鸟辅,所以,只需要初始化一次OpenGL渲染窗口莺葫。而且場景的流程控制功能匪凉,也只需要存在一個(gè)這樣的場景控制對(duì)象即可。為了保證CCDirector類只存在一個(gè)實(shí)例對(duì)象捺檬,就必須使用單例模式再层。
CCTextureCache單例:
此類主要負(fù)責(zé)加載游戲當(dāng)中所需要的紋理圖片資源,這些資源加載好以后堡纬,就可以一直保留在內(nèi)存里面聂受,當(dāng)下次再需要使用此紋理的時(shí)候,直接返回相應(yīng)的紋理對(duì)象引用就可以了烤镐,無需再重復(fù)加載蛋济。當(dāng)然,這樣做可能會(huì)很浪費(fèi)內(nèi)存炮叶,所以cocos2dx采用了一種引用計(jì)數(shù)的方式來管理對(duì)象內(nèi)存碗旅,當(dāng)紋理剛被加載進(jìn)來的時(shí)候,引用計(jì)數(shù)為1悴灵。如果使用此紋理對(duì)象創(chuàng)建一個(gè)精靈扛芽,那么此紋理對(duì)象引用會(huì)加1.如果精靈被釋放,則相應(yīng)的引用計(jì)數(shù)減1.當(dāng)紋理的引用計(jì)數(shù)變?yōu)?的時(shí)候积瞒,紋理所占用的內(nèi)存自然就會(huì)被釋放掉川尖。這也是為什么在收到內(nèi)存警告的時(shí)候,會(huì)調(diào)用CCTextureCache的removeUnusedTextures方法茫孔。此方法會(huì)將所有引用計(jì)數(shù)為1的紋理對(duì)象全部釋放掉叮喳。單從字面上看被芳,Cache,即緩存的意思馍悟。它以犧牲一定的內(nèi)存壓力為代價(jià)畔濒,帶來的是游戲性能的提升。這種cache技術(shù)锣咒,在游戲開發(fā)中比比皆是侵状。注:IO操作對(duì)游戲性能影響非常大,要極力避免R阏Hば帧!
擴(kuò)展:
類似的CCSpriteFrameCache悼嫉、CCAnimationCache和CCShaderCache艇潭,它們也都是緩存類,分別負(fù)責(zé)緩存SpriteFrame戏蔑、Animation和Shader蹋凝。這樣做的原因無非就是為了性能,以空間換時(shí)間.
CCUserDefault單例:
此類主要是用來保存游戲中的數(shù)據(jù)用的总棵,它會(huì)創(chuàng)建一個(gè)xml文件鳍寂,并把用戶自定義的數(shù)據(jù)以key-value的形式存儲(chǔ)到此xml文件中。此類為什么會(huì)變成單例類呢彻舰?原因也很簡單伐割,因?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接口的對(duì)象引用橄碾,它可以幫助我們方便地找到LuaEngine對(duì)象。這里單例的作用純粹變成了LuaEngine的一個(gè)全局訪問點(diǎn)了。
為什么不直接把LuaEngine作為單例對(duì)象呢?
是否在某些情況之下法牲,可能需要?jiǎng)?chuàng)建多個(gè)LuaEngine對(duì)象史汗?如果考慮到cocos2d-x還可以同時(shí)支持其它的腳本引擎,那也可以相應(yīng)的把另外的腳本引擎設(shè)計(jì)成單例類拒垃。當(dāng)然停撞,這樣做無疑會(huì)使引擎里面的單例過多〉课停考慮到單例模式近年來被廣大開發(fā)者所詬病戈毒,已將其列入“反模式”。這里引用CCScriptEngineManager單例類谤牡,給其它引擎對(duì)象提供訪問的惟一全局點(diǎn)副硅,這也不失為一個(gè)辦法姥宝。不知我的推測是否正確翅萤?
CCFileUtils類:
該類是一個(gè)工具類。工具類和配置文件類腊满,它們絕大多數(shù)情況也都是設(shè)計(jì)成單例的套么。因?yàn)樗鼈儧]有存在多個(gè)實(shí)例的必要。同時(shí)碳蛋,它們也可以實(shí)現(xiàn)為一組類方法胚泌,這樣無需創(chuàng)建對(duì)象也能夠使用。
SimpleAudioEngine類:
它也被設(shè)計(jì)成了一個(gè)單例類肃弟。因?yàn)樗峁┙o了開發(fā)人員最簡單的聲音操作接口玷室,可以方便地處理游戲中的背景音樂和音效。此類同時(shí)還應(yīng)用了外觀模式笤受,把CocoDenshion子系統(tǒng)中的復(fù)雜功能給屏蔽起來了穷缤,簡化了客戶端程序員的調(diào)用箩兽。該類為什么要設(shè)計(jì)成單例汗贫,是因?yàn)榈教幎家L問它。設(shè)計(jì)成單例會(huì)很方便部蛇,而且它與其它對(duì)象沒有什么聯(lián)系涯鲁,不好使用對(duì)象組合撮竿。
2.使用單例模式的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):簡單易用,限制一個(gè)類只有一個(gè)實(shí)例髓需,可以減少創(chuàng)建多個(gè)對(duì)象可能會(huì)引起的內(nèi)存問題的風(fēng)險(xiǎn)僚匆,包括內(nèi)存泄漏咧擂、內(nèi)存占用問題檀蹋。
缺點(diǎn):單例模式因?yàn)樘峁┝艘粋€(gè)全局的訪問點(diǎn)俯逾,你可以在程序的任何地方輕而易取地訪問到贸桶,這本身就是一種高耦合的設(shè)計(jì)。一旦單例改變以后桌肴,其它模板都需要修改皇筛。另外,單例模式使得對(duì)象變成了全局的了坠七。學(xué)過面對(duì)對(duì)象編程的人都知道水醋,全局變量是非常邪惡的,要盡量不要使用彪置。而且單例模式會(huì)使得對(duì)象的內(nèi)存在程序結(jié)束之前一直存在拄踪,在一些使用GC的語言里面,這其實(shí)就是一種內(nèi)存泄漏宫蛆,因?yàn)樗鼈冇肋h(yuǎn)都不到釋放。當(dāng)然耀盗,也可以通過提供一些方法來釋放單例對(duì)象所占用的內(nèi)存卦尊,比如前面提到的XXXCache對(duì)象,都有相應(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í)候,才能考慮逐样。不要為了訪問方便挪捕,就把任何類都弄成單例滞乙,這樣,到最后硬耍,你會(huì)發(fā)現(xiàn)你的程序里面就只剩下一堆單例和工廠了墩朦。此外,單例模式正在消減,比如CCActionManager和CCTouchDispatcher在cocos2d1.0之前也是單例,現(xiàn)在變成了CCDirector類的屬性了凰荚。而且Riq(cocos2d-iphone的作者)也有在郵件中提到燃观,以后CCDirector對(duì)象也會(huì)變成非單例,并且允許一個(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è)單例對(duì)象非春。我武斷地認(rèn)為柱徙,客戶端游戲開發(fā)中缓屠,至少需要一個(gè)單例對(duì)象。因?yàn)橐粋€(gè)全局的訪問點(diǎn)可以方便很多對(duì)象之間的交互护侮。根據(jù)之前的討論敌完,也可以把一些時(shí)覺需要用到的類引用保存在此單例對(duì)象中,不過只需要保存弱引用即可羊初。使用單例滨溉,最嚴(yán)重的就是怕內(nèi)存泄漏,所以长赞,大家盡量不要把單例類設(shè)計(jì)地太復(fù)雜晦攒,也不要讓它包含過多的動(dòng)態(tài)內(nèi)存管理工作。
二段構(gòu)建模式
所謂二段構(gòu)建得哆,就是指創(chuàng)建對(duì)象時(shí)不是直接通過構(gòu)建函數(shù)來分配內(nèi)存并完成初始化操作脯颜。取而代之的是,構(gòu)造函數(shù)只負(fù)責(zé)分配內(nèi)存贩据,而初始化的工作則由一些名為initXXX的成員方法來完成栋操。然后再定義一些靜態(tài)類方法把這兩個(gè)階段組合起來,完成最終對(duì)象的構(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)建的過程她倘,請(qǐng)看下列代碼:
//此方法現(xiàn)在已經(jīng)不推薦使用了璧微,將來可能會(huì)刪除
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(); //G傲颉!荧止!額外做了內(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ù)也有初始化的功能牧愁,所以,我們?cè)賮砜纯碈CSprite的構(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系列方法中偷线,大家可以動(dòng)手去查看源代碼磨确。
2.分析為什么要使用此模式?
這種二段構(gòu)建對(duì)于C++程序員來說声邦,其實(shí)有點(diǎn)別扭乏奥。因?yàn)閏++的構(gòu)造函數(shù)在設(shè)計(jì)之初就是用來分配內(nèi)存+初始化對(duì)象的。如果再搞個(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)建并沒有什么不好,只是更加突出了對(duì)象需要初始化绞铃。在某種程度上也可以說是一種設(shè)計(jì)強(qiáng)化镜雨。因?yàn)橥洺跏蓟且磺心涿畹腷ug的罪魁禍?zhǔn)住M瑫r(shí)儿捧,二段構(gòu)建出來的對(duì)象都是autorelease的對(duì)象荚坞,而autorelease對(duì)象是使用引用計(jì)數(shù)來管理內(nèi)存的挑宠。客戶端程序員在使用此接口創(chuàng)建對(duì)象的時(shí)候西剥,無需關(guān)心具體實(shí)現(xiàn)細(xì)節(jié)痹栖,只要知道使用create方法可以創(chuàng)建并初始化一個(gè)自動(dòng)釋放內(nèi)存的對(duì)象即可。
在一點(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è)新的對(duì)象。比如Boolean.valueOf(boolean)钮蛛。
3.它還可以返回原類型的子類型對(duì)象鞭缭。
因此,使用二段構(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一般不會(huì)忘記分配內(nèi)存,但卻常常忽略初始化的作用驼壶。
2.見上面分析《Effective Java》的第1條:“為每一個(gè)類提供一個(gè)靜態(tài)工廠方法來代替構(gòu)造函數(shù)”
3.除了完成對(duì)象構(gòu)建氏豌,還可以管理對(duì)象內(nèi)存。
缺點(diǎn):
1.不如直接使用構(gòu)造函數(shù)來得直白热凹、明了箩溃,違反直覺,但這個(gè)是相對(duì)的碌嘀。
4.此模式的定義及一般實(shí)現(xiàn)定義:將一個(gè)對(duì)象的構(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(){
//這里初始化對(duì)象成員
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)始人王哲對(duì)于為什么要設(shè)計(jì)成二段構(gòu)建的看法:“其實(shí)我們?cè)O(shè)計(jì)二段構(gòu)造時(shí)首先考慮其優(yōu)勢而非兼容cocos2d-iphone. 初始化時(shí)會(huì)遇到圖片資源不存在等異常塔淤,而C++構(gòu)造函數(shù)無返回值摘昌,只能用try-catch來處理異常,啟用try-catch會(huì)使編譯后二進(jìn)制文件大不少高蜂,故需要init返回bool值聪黎。Symbian, Bada SDK,objc的alloc + init也都是二階段構(gòu)造”