1. 程式人生 > >Cocos2d-x設計模式發掘之一:單例模式

Cocos2d-x設計模式發掘之一:單例模式

本系列文章我將和大家一起來發掘cocos2d-x中所使用到的設計模式,同樣的,這些模式在cocos2d-iphone中也可以找到其身影。

宣告:這裡發掘模式只是我的個人愛好,通過這個過程,我希望能加深自己對於設計模式運用的理解。關於模式的學習,市面上已經有許多非常好的書籍了。比如《Head First設計模式》、GoF的設計模式,還有《研磨設計模式》等。如果讀者對於設計模式完全不瞭解的話,建議先閱讀上面至少一本書籍,瞭解設計模式之後再閱讀本系列文章。這樣大家才會有相互交流的共同語言。

為什麼要發掘設計模式呢?因為設計模式本身就是人們在一些面向物件的軟體系統裡面發掘出來的,在一定的場景之下可以重用的解決方案。通過對模式的挖掘,我可以藉此機會學習一下這些優秀的設計思想。因為我覺得,一個好的開源遊戲框架除了能給我們開發者帶來開發效率的提升以外,還應該被我們吸收其設計思想,這樣它的價值才能完整。

本系列文章將按照如下思路進行模式挖掘: – 找出某個設計模式的應用場景(類、類結構、物件結構,必要時結合UML類圖) – 分析為什麼要使用此模式(即設計決策) – 使用此模式的優缺點是什麼(任何事務都有兩面性,設計模式也不例外) – 此模式的定義及一般實現(這個在GoF的經典書籍裡面有,這裡借用一下) – 在遊戲開發中如何運用此模式(自己對於模式運用場景的理解) – 此模式經常與哪些模式配合使用(這個也基本是從GoF的書籍裡面借用了)

1、應用場景

Cocos2D-x中的單例如下:CCDirector,CCTextureCache,CCSpriteFrameCache,CCAnimationCache,CCUserDefault,CCNotificationCenter,CCShaderCache,CCScriptEngineManager,CCPoolManager,CCFileUtils,CCProfiler,SimleAudioEngie,CCConfiguration,CCApplication,CCDirectorCaller(ios平臺),CCEGLView。

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

首先是CCDirector單例,它負責管理初始化OpenGL渲染視窗以及遊戲場景的流程控制,它是cocos2dx遊戲開發中必不可少的類之一。為什麼要把此類設計成單例物件呢?因為,一個遊戲只需要有一個遊戲視窗就夠了,所以,只需要初始化一次OpenGL渲染視窗。而且場景的流程控制功能,也只需要存在一個這樣的場景控制物件即可。為了保證CCDirector類只存在一個例項物件,就必須使用單例模式。

接下來是CCTextureCache單例。此類主要負責載入遊戲當中所需要的紋理圖片資源,這些資源載入好以後,就可以一直保留在記憶體裡面,當下次再需要使用此紋理的時候,直接返回相應的紋理物件引用就可以了,無需再重複載入。當然,這樣做可能會很浪費記憶體,所以cocos2dx採用了一種引用計數的方式來管理物件記憶體,當紋理剛被載入進來的時候,引用計數為1。如果使用此紋理物件建立一個精靈,那麼此紋理物件引用會加1.如果精靈被釋放,則相應的引用計數減1.當紋理的引用計數變為0的時候,紋理所佔用的記憶體自然就會被釋放掉。這也是為什麼在收到記憶體警告的時候,會呼叫CCTextureCache的removeUnusedTextures方法。此方法會將所有引用計數為1的紋理物件全部釋放掉。單從字面上看,Cache,即快取的意思。它以犧牲一定的記憶體壓力為代價,帶來的是遊戲效能的提升。這種cache技術,在遊戲開發中比比皆是。注:IO操作對遊戲效能影響非常大,要極力避免!!!

類似的CCSpriteFrameCache、CCAnimationCache和CCShaderCache,它們也都是快取類,分別負責快取SpriteFrame、Animation和Shader。這樣做的原因無非就是為了效能,以空間換時間。

接下來,看看CCUserDefault。此類主要是用來儲存遊戲中的資料用的,它會建立一個xml檔案,並把使用者自定義的資料以key-value的形式儲存到此xml檔案中。此類為什麼會變成單例類呢?原因也很簡單,因為類似這種操作資料檔案,或者配置檔案的類,通常只需要在程式執行過程中存在一個例項即可。

接著是CCNotificationCenter,這是一個通知中心,它其實還運用了一個觀察者模式,這裡暫時不討論。該通知中心理論上也是隻需要一個就夠了。但是,cocos2d-x在實現此單例的時候,並沒有將此類的建構函式私有麼,我在猜想,是不是開發人員有意為之呢?或者多個通知中心也有其存在的價值。這個大家可以討論一下。

CCScriptEngineManager,此類包含一個實現了CCScriptEngineProtocl介面的物件引用,它可以幫助我們方便地找到LuaEngine物件。這裡單例的作用純粹變成了LuaEngine的一個全域性訪問點了。為什麼不直接把LuaEngine作為單例物件呢?是否在某些情況之下,可能需要建立多個LuaEngine物件?如果考慮到cocos2d-x還可以同時支援其它的指令碼引擎,那也可以相應的把另外的指令碼引擎設計成單例類。當然,這樣做無疑會使引擎裡面的單例過多。考慮到單例模式近年來被廣大開發者所詬病,已將其列入“反模式”。這裡引用CCScriptEngineManager單例類,給其它引擎物件提供訪問的惟一全域性點,這也不失為一個辦法。不知我的推測是否正確?

CCPoolManager,此類是用來管理AutoReleasePool物件棧的。因為cocos2d-x採用的是基於引用計數的方式來管理動態記憶體,所以採用引用計數的被託管物件都被放入一個當前的autoReleasePool裡面去了。當CCDirector的mainLoop每次更新的時候,都會呼叫CCPoolManager的pop方法,把當前autoReleasePool裡面的所有autoRelease物件的託管狀態設定為false,同時把該autoReleasePool清空,而清空的時候則會呼叫autoReleasePool裡面所有物件的release方法來釋放記憶體。此類為什麼要設計與單例呢?因為多個地方需要引用此類,為了方便引用,所以設計成單例。

然後是CCFileUtils類。該類是一個工具類。工具類和配置檔案類,它們絕大多數情況也都是設計成單例的。因為它們沒有存在多個例項的必要。同時,它們也可以實現為一組類方法,這樣無需建立物件也能夠使用。

然後是CCProfiler類,該類負責cocos2d的效能其執行情況分析,也是一個工具類。所以它設計成單例類的理由與CCFileUtils類差不多。

CCConfiguration類也被設計成了單例物件,此類主要負責管理cocos2d-x的一些OpenGL變數資訊。這些資訊本可以通過定義一些巨集,或者通過一些全域性變數來解決的。這裡設計成單例類也是更加“面向物件”的體現。因為這些配置資訊根本不需存在多個物件。

CCApplication類的設計初衷是獲得平臺相關的一些資訊,最重要的是運行遊戲的主迴圈(main loop)。一個遊戲只需要一個應用程式例項,所以設計與單例。

CCEGLView是Cocos2d-x對於EGLView的抽象,不同的平臺會有不同的實現,使之可以適用不同的平臺。在ios平臺上面它是對EAGLView的一個簡單的封裝。該類表示的是對OpenGL渲染上下文視窗的一種抽象,這是一種虛擬資源,而且只有存在一份例項的可能,所以被設計成單例類。

CCDirectorCaller類是ios平臺相關的類,就是對ios平臺CCDirector物件的一個封裝,使用的是CADisplayLink來運行遊戲主迴圈。該類和CCDirector類差不多,也可以設計成單例。為什麼會在CCApplication類裡面呼叫CCDirectorCaller類,是基於分離平臺相關程式碼的考慮。CCApplication是的,CCDirectorCaller也是的。

最後一個是SimpleAudioEngine類,它也被設計成了一個單例類。因為它提供給了開發人員最簡單的聲音操作介面,可以方便地處理遊戲中的背景音樂和音效。此類同時還應用了外觀模式,把CocoDenshion子系統中的複雜功能給遮蔽起來了,簡化了客戶端程式設計師的呼叫。該類為什麼要設計成單例,是因為到處都要訪問它。設計成單例會很方便,而且它與其它物件沒有什麼聯絡,不好使用物件組合。

2.使用單例模式的優缺點

優點:

1)簡單易用,限制一個類只有一個例項,可以降低建立多個物件可能會引起的記憶體問題的風險,包括記憶體洩漏、記憶體佔用問題。

缺點:

單例模式因為提供了一個全域性的訪問點,你可以在程式的任何地方輕而易取地訪問到,這本身就是一種高耦合的設計。一旦單例改變以後,其它模組都需要修改。另外,單例模式使得物件變成了全域性的了。學過面對物件程式設計的人都知道,全域性變數是非常邪惡的,要儘量不要使用。而且單例模式會使得物件的記憶體在程式結束之前一直存在,在一些使用GC的語言裡面,這其實就是一種記憶體洩漏,因為它們永遠都不到釋放。當然,也可以通過提供一些特殊的方法來釋放單例物件所佔用的記憶體,比如前面提到的XXXCache物件,都有相應的Purge方法。最後,cocos2dx裡面實現的單例,99%都不是執行緒安全的。

在討論優缺點的時候,讀者想必也看出來了,缺點比優點多多了。這是給大家提個醒,以後使用單例模式的時候要謹慎,不要濫用。因為此模式最容易被濫用。只有真正符合單例模式應用場景的時候,才能考慮。不要為了訪問方便,就把任何類都弄成單例,這樣,到最後,你會發現你的程式裡面就只剩下一堆單例和工廠了。

此外,單例模式正在消減,比如CCActionManager和CCTouchDispatcher在cocos2d1.0之前也是單例,現在變成了CCDirector類的屬性了。而且Riq(cocos2d-iphone的作者)也有在郵件中提到,以後CCDirector物件也會變成非單例,並且允許一個遊戲中建立多個遊戲視窗。

3.單例模式的定義:保證一個類僅有一個例項,並提供一個訪問它的全域性的訪問點。

UML圖:

它的一般實現如下所示:

Singleton
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton
{
   public:
   //全域性訪問點
   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;

注意,這裡只是最基本的實現,它沒有考慮到執行緒安全,也沒有考慮記憶體釋放。但是,這個實現有兩個最基本的要素。一:定義一個靜態變數,並把建構函式等設定為私有的。二:提供一個全域性的訪問點給外部訪問。

4.遊戲開發中如何運用此模式呢?眾所周知,遊戲開發中離不開遊戲資料儲存和載入。這些資料包括關卡資料、遊戲進行中的狀態資料等。這樣一些資訊很多遊戲模組中都需要訪問,所以可以為之設定一個單例物件。我武斷地認為,客戶端遊戲開發中,至少需要一個單例物件。因為一個全域性的訪問點可以方便很多物件之間的互動。根據之前的討論,也可以把一些時覺需要用到的類引用儲存在此單例物件中,不過只需要儲存弱引用即可。使用單例,最嚴重的就是怕記憶體洩漏,所以,大家儘量不要把單例類設計地太複雜,也不要讓它包含過多的動態記憶體管理工作。

5.單例模式一般與工廠模式配合使用,因為一般會將工廠類設計成單例物件。比如前面提到的各種cache類,它們也可以看作是某種意義上的工廠物件。由於工廠就是負責生產物件的,而cache類都可以根據使用者的需要生產出相應的物件。

最後,看看cocos2d-x創始人王哲對於什麼是單例的看法:“這麼說吧, 我設計成單例基本就一種抽象情況:獨佔性資源。比如某個硬體IO (如CCTouchDispatcher, CCAccelerometer),比如公用的快取區(CCTextureCache, CCUserDefault)。後來有人抱怨單例類太多,想銷燬整個cocos2d instance再重建很麻煩,所以小明和riq就把大量單例類放到CCDirector裡面管理。”

歡迎讀者批評指正,如果有興趣跟我一起挖掘cocos2d-x中所涉及到的設計模式的朋友,可以給我發郵件:[email protected]或者直接留言。