單例模式

什麼時候需要使用單例模式呢?

正如它的名字一樣,你認爲一些東西在整個遊戲中只有一個而你又想可以方便地隨時訪問它,這時你就可以考慮單例模式了。例如,你的遊戲可能需要一個管理音樂播放的腳本,或者一個管理場景切換的腳本,或者一個管理玩家信息的通用腳本,又或者是管理遊戲中各種常用UI的腳本。事實上,這些都是非常常用而且必要的。



實現


慶幸的是,單例模式的代碼非常簡單。下面是Singleton.cs的內容:
[csharp] view plain copy
 print?
  1. using System;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4.    
  5.    
  6. public class Singleton : MonoBehaviour  
  7. {  
  8.     private static GameObject m_Container = null;  
  9.     private static string m_Name = "Singleton";  
  10.     private static Dictionary<stringobject> m_SingletonMap = new Dictionary<stringobject>();  
  11.     private static bool m_IsDestroying = false;  
  12.        
  13.     public static bool IsDestroying  
  14.     {  
  15.         get { return m_IsDestroying; }  
  16.     }  
  17.        
  18.     public static bool IsCreatedInstance(string Name)  
  19.     {  
  20.         if(m_Container == null)  
  21.         {  
  22.             return false;  
  23.         }  
  24.         if (m_SingletonMap!=null && m_SingletonMap.ContainsKey(Name))   
  25.         {  
  26.             return true;  
  27.         }  
  28.         return false;  
  29.            
  30.     }  
  31.     public static object getInstance (string Name)  
  32.     {  
  33.         if(m_Container == null)  
  34.         {  
  35.             Debug.Log("Create Singleton.");  
  36.             m_Container = new GameObject ();  
  37.             m_Container.name = m_Name;      
  38.             m_Container.AddComponent (typeof(Singleton));  
  39.         }  
  40.         if (!m_SingletonMap.ContainsKey(Name)) {  
  41.             if(System.Type.GetType(Name) != null)  
  42.             {  
  43.                 m_SingletonMap.Add(Name, m_Container.AddComponent (System.Type.GetType(Name)));  
  44.             }  
  45.             else  
  46.             {  
  47.                 Debug.LogWarning("Singleton Type ERROR! (" + Name + ")");  
  48.             }  
  49.         }  
  50.         return m_SingletonMap[Name];  
  51.     }     
  52.        
  53.     public static void RemoveInstance(string Name)  
  54.     {  
  55.         if (m_Container != null && m_SingletonMap.ContainsKey(Name))  
  56.         {  
  57.             UnityEngine.Object.Destroy((UnityEngine.Object)(m_SingletonMap[Name]));  
  58.             m_SingletonMap.Remove(Name);  
  59.               
  60.             Debug.LogWarning("Singleton REMOVE! (" + Name + ")");  
  61.         }  
  62.     }  
  63.    
  64.     void Awake ()  
  65.     {  
  66.         Debug.Log("Awake Singleton.");  
  67.         DontDestroyOnLoad (gameObject);  
  68.     }  
  69.        
  70.     void Start()  
  71.     {  
  72.         Debug.Log("Start Singleton.");  
  73.     }     
  74.        
  75.     void Update()  
  76.     {  
  77.     }  
  78.        
  79.     void OnApplicationQuit()  
  80.     {  
  81.         Debug.Log("Destroy Singleton");  
  82.         if(m_Container != null)  
  83.         {  
  84.             GameObject.Destroy(m_Container);  
  85.             m_Container = null;  
  86.             m_IsDestroying = true;  
  87.         }             
  88.     }  
  89.        
  90. }  

代碼大部分都比較容易看懂,下面介紹幾點注意的地方:
  • 當我們在其他代碼裏需要訪問某個單例時,只需調用getInstance函數即可,參數是需要訪問的腳本的名字。我們來看一下這個函數。它首先判斷所有單例所在的容器m_Container是否爲空(實際上就是場景中是否存在一個Gameobject,上面捆綁了一個Singleton腳本),如果爲空,它將自動創建一個對象,然後以“Singleton”命名,再捆綁Singleton腳本。m_SingletonMap是負責維護所有單例的映射。當第一次訪問某個單例時,它會自動向m_Container上添加一個該單例類型的Component,並保存在單例映射中,再返回這個單例。因此,我們可以看出,單例的創建完全都是自動的,你完全不需要考慮在哪裏、在什麼時候捆綁腳本,這是多麼令人高興得事情!
  • Awake函數中,有一句代碼DontDestroyOnLoad (gameObject);,這是非常重要的,這句話意味着,當我們的場景發生變化時,單例模式將不受任何影響。除此之外,我們還要注意到,這句話也必須放到Awake函數,而不能放到Start函數中,這是由兩個函數的執行順序決定的,如果反過來,便可能會造成訪問單例不成功,下面的例子裏會更詳細的介紹;
  • OnApplicationQuit函數中,我們將銷燬單例模式。
  • 最後一點很重要:一定不要在OnDestroy函數中直接訪問單例模式!這樣很有可能會造成單例無法銷燬。這是因爲,當程序退出準備銷燬單例模式時,我們在其他腳本的OnDestroy函數中再次請求訪問它,這樣將重新構造一個新的單例而不會被銷燬(因爲之前已經銷燬過一次了)。如果一定要訪問的話,一定要先調用IsCreatedInstance,判斷該單例是否存在。


例子


下面,我們通過一個小例子來演示單例模式的使用。
首先,我們需要創建如上的Singleton腳本。然後,再創建一個新的腳本SingletonSample.cs用於測試,其內容如下:
[csharp] view plain copy
 print?
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class SingletonSample : MonoBehaviour {  
  5.   
  6.     // Use this for initialization  
  7.     void Start () {  
  8.         TestSingleton();  
  9.     }  
  10.       
  11.     // Update is called once per frame  
  12.     void Update () {  
  13.       
  14.     }  
  15.       
  16.     private void TestSingleton() {  
  17.         LitJsonSample litjson = Singleton.getInstance("LitJsonSample"as LitJsonSample;  
  18.           
  19.         litjson.DisplayFamilyList();  
  20.     }  
  21.       
  22. //  void OnDestroy() {  
  23. //      LitJsonSample litjson = Singleton.getInstance("LitJsonSample") as LitJsonSample;  
  24. //        
  25. //      litjson.DisplayFamilyList();  
  26. //  }  
  27. }  

注意,爲了方便,我使用了上一篇博文裏使用的Litjson的代碼,並做了少許修改。下面是修改後的LitJsonSample.cs:
[csharp] view plain copy
 print?
  1. using UnityEngine;  
  2. using UnityEditor;  
  3. using System.Collections;  
  4. using System.Collections.Generic;  
  5. using LitJson;  
  6.   
  7. public class FamilyInfo {  
  8.     public string name;  
  9.     public int age;  
  10.     public string tellphone;  
  11.     public string address;  
  12. }  
  13.   
  14. public class FamilyList {  
  15.     public List<FamilyInfo> family_list;  
  16. }  
  17.   
  18. public class LitJsonSample : MonoBehaviour {  
  19.       
  20.     public FamilyList m_FamilyList = null;  
  21.       
  22.     // Use this for initialization  
  23.     void Awake () {  
  24.         ReloadFamilyData();  
  25.     }  
  26.       
  27.     private void ReloadFamilyData()  
  28.     {  
  29.         //AssetDatabase.ImportAsset("Localize/family.txt");  
  30.               
  31.         UnityEngine.TextAsset s = Resources.Load("Localize/family"as TextAsset;   
  32.         string tmp = s.text;  
  33.         m_FamilyList = JsonMapper.ToObject<FamilyList>( tmp );  
  34.         if ( JsonMapper.HasInterpretError() )  
  35.         {  
  36.             Debug.LogWarning( JsonMapper.GetInterpretError() );  
  37.         }  
  38.     }  
  39.       
  40.     public void DisplayFamilyList() {  
  41.         if (m_FamilyList == nullreturn;  
  42.           
  43.         foreach (FamilyInfo info in m_FamilyList.family_list) {  
  44.             Debug.Log("Name:" + info.name + "       Age:" + info.age + "        Tel:" + info.tellphone + "      Addr:" + info.address);  
  45.         }  
  46.     }  
  47.       
  48.     // Update is called once per frame  
  49.     void Update () {  
  50.       
  51.     }  
  52. }  


然後,將SingletonSample.cs添加到場景中的一個對象上。我偷懶就直接添加到了攝像機上。注意,其他兩個代碼不要添加到任何對象上。

運行結果如圖:


爲了證明之前所說的不要在OnDestroy函數裏訪問單例模式,我們把SingletonSample.cs腳本里註釋掉得OnDestroy函數解開註釋,然後再次運行。結果如下:


我們注意到,除了Log頁面裏出現了錯誤信息外,右側的場景面板裏也多了一個Singleton對象(這是我已經停止運行了)。從Log信息裏,我們可以發現,在第一次銷燬掉單例模式後,單例模式又再次被創建,但卻沒有被銷燬,由此便殘留在了面板裏。

正確的做法是,在OnDestroy函數里加一層安全性判斷,如下:
[csharp] view plain copy
 print?
  1. void OnDestroy() {  
  2.     if (Singleton.IsCreatedInstance("LitJsonSample")) {  
  3.         LitJsonSample litjson = Singleton.getInstance("LitJsonSample"as LitJsonSample;  
  4.               
  5.         litjson.DisplayFamilyList();  
  6.     }  
  7. }  


這樣,就可以得到正確結果了。

結束語


最後,還有幾句話要囉嗦一下,雖然和單例模式的關係不大,嘿嘿。我們需要注意一下Start函數和Awake函數的執行順序。在這個例子裏,我在LitJsonSample.cs的Awake函數裏調用了ReloadFamilyData來初始化數據,細心的童鞋可以發現,在上一篇博文裏,初始化數據是在Start函數裏完成的。之所以要把它挪到Awake函數裏,是爲了在我們訪問單例時,可以保證數據一定已經被初始化了,因此把初始化函數放到Awake函數裏,訪問單例的代碼放在Start函數裏。同樣的原因,在Singleton.cs的腳本里DontDestroyOnLoad (gameObject);需要放在Awake函數,而不是Start函數裏。
關於Awake函數和Start函數的執行順序,可以詳見腳本說明。簡單來說,Awake函數在這個腳本在場景中加載時就會調用,至於所有腳本的Awake函數的調用順序是未知的。然後,在所有的Awake函數調用完畢後,纔開始調用Start函數。需要注意的是,Start函數也不是一定立即執行的,它是在該腳本第一次調用Update函數之前調用的,也就是說,如果這個腳本一開始的狀態是disable的,那麼直到它變成enable狀態,在Update函數第一次執行前,纔會執行Start函數。兩個函數的執行順序是時間有時正是某些Bug的產生原因!而且這些Bug往往很難發現。
哈,我這次實習的面試時,面試的姐姐就問過我這個問題,希望大家也可以搞清楚,如果我這裏有說的不對的,請指正。

好啦,這次就到這裏,謝謝閱讀!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章