為什麼要使用單例模式
在我們的整個遊戲生命周期當中,有很多對象從始至終有且隻有一個。這個唯一的執行個體隻需要生成一次,并且直到遊戲結束才需要銷毀。
單例模式一般應用于管理器類,或者是一些需要持久化存在的對象。
Unity3d中單例模式的實作方式
(一)c#當中實作單例模式的方法
因為單例本身的寫法不是重點,是以這裡就略過,直接上代碼。
以下代碼來自于MSDN。
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
以上代碼是比較完整版本的c#單例。在unity當中,如果不需要使用到monobeheviour的話,可以使用這種方式來建構單例。
(二)如果是MonoBeheviour呢?
MonoBeheviour和一般的類有幾個重要差別,展現在單例模式上有兩點。
第一,MonoBehaviour不能使用構造函數進行執行個體化,隻能挂載在GameObject上。
第二,當切換場景時,目前場景中的GameObject都會被銷毀(LoadLevel帶有additional參數時除外),這種情況下,我們的單例對象也會被銷毀。
為了使之不被銷毀,我們需要進行DontDestroyOnLoad的處理。同時,為了保持場景當中隻有一個執行個體,我們要對目前場景中的單例進行判斷,如果存在其他的執行個體,則應該将其全部删除。
是以,建構單例的方式會變成這樣。
public sealed class SingletonMoBehaviour: MonoBehaviour
{
private static volatile SingletonBehaviour instance;
private static object syncRoot = new Object();
public static SingletonBehaviour Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null) {
SingletonBehaviour[] instances = FindObjectsOfType<SingletonBehaviour>();
if (instances != null){
for (var i = 0; i < instances.Length; i++) {
Destroy(instances[i].gameObject);
}
}
GameObject go = new GameObject("_SingletonBehaviour");
instance = go.AddComponent<SingletonBehaviour>();
DontDestroyOnLoad(go);
}
}
}
return instance;
}
}
}
這種方式并非完美。其缺陷至少有:
* 如果有許多的單例類,會需要複制粘貼這些代碼
* 有些時候我們也許會希望使用目前存在的所有執行個體,而不是删除全部建立一個執行個體。(這個未必是缺陷,隻是設計的不同)
在本文後面将會附上這種單例模式的代碼以及測試。
(三)使用模闆類實作單例
為了避免重複代碼,我們可以使用模闆類的方式來生成單例。非MonoBehaviour的實作方式這裡就不贅述,隻說monoBehaviour的。
代碼
public sealed class SingletonTemplate<T> : MonoBehaviour where T : MonoBehaviour
{
private static volatile T instance;
private static object syncRoot = new Object();
public static T Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
T[] instances = FindObjectsOfType<T>();
if (instances != null)
{
for (var i = 0; i < instances.Length; i++)
{
Destroy(instances[i].gameObject);
}
}
GameObject go = new GameObject();
go.name = typeof(T).Name;
instance = go.AddComponent<T>();
DontDestroyOnLoad(go);
}
}
}
return instance;
}
}
}
以上代碼解決了每個單例類都需要重複寫同樣代碼的問題,基本上算一個比較好的解決方案。
單例當中的一些坑
- 最大的坑是單例的monobehaviour,其生命周期并非我們程式員可以控制的。MonoBehaviour本身的Destroy,将會決定單例類的執行個體在何時銷毀。是以,一定不要在OnDestroy函數中調用單例對象,這可能導緻該對象在遊戲結束後依然存在(原本的單例類已經銷毀了,你又建立了一個新的,當然就不會再銷毀一次了)。舉例來說,以下的代碼是需要注意的的。
void Start(){
Singleton.Instance.OnSomeTime += DoSth;
}
void OnDestroy(){
Singleton.Instance.OnSomeTime -= DoSth;
}
- 此外,建議不要在場景或者預置當中放置擁有單例類元件的Gameobject。很多網上的項目有這樣的寫法。但我的觀點是這種寫法不夠靈活。如果使用這種方法,注意在擷取instance時,将找到的第一個對象賦給instance
public static T Instance
{
get
{
if (instance == null)
{
T[] instances = FindObjectsOfType<T>();
if (instances != null)
{
instance = instances[0];
for (var i = 1; i < instances.Length; i++)
{
Destroy(instances[i].gameObject);
}
}
}
return instance;
}
}
單例與靜态的差別
我們都知道,靜态的成員或者方法,在整個Runtime當中也隻有一份。是以一直存在着靜态與單例模式之争。
事實上這兩種方式都有其适用範圍,不能片面的說某種好或某種不好。具體的争論實在是太多了,資料也多,這裡也不深入講,僅僅簡單的說明一下兩者使用上的差別。
* 單例的方法可以繼承,靜态的不可以。
* 單例存在着建立執行個體的過程,生命周期并不是整個運作時,靜态方法在編譯時就存在,整個過程中是一直有效的。
雖然兩者的差別其實非常多,但在這裡隻說一個最核心的問題,如何進行選擇?
其實很簡單,從面向對象的角度來說——
* 如果方法中需要用到執行個體本身的狀态,也就是說需要用到執行個體的成員時,這個方法一定是執行個體方法,請使用單例調用。
* 如果方法中完全不涉及到執行個體,而是類共享的一些狀态的話,或者甚至不需要任何狀态,這個方法一定是靜态方法。
從應用的角度來說,我覺得以上就足夠了,至于說記憶體占用的不同啊,GC以及效率上的差別啊這些我覺得更多是理論,不夠貼近實際使用。
單例雖好,請勿濫用
濫用設計模式是很多人都會遇到的問題,尤其是對新手來說。設計模式應該隻在合适的場景當中使用,而不是随處都使用單例。
事實上,單例的濫用會造成以下一些問題:
* 代碼的耦合性可能會增加。如一個子產品當中調用MusicController.instance.Play,可能導緻這個子產品無法獨立複用。
* 單個類的職責可能會過大,違背單一職責原則。
* 某些情況下會造成一些性能問題。因為單例的對象永遠不銷毀,過多的單例會造成性能問題。
可以使用一些别的方法來代替單例模式,這裡暫時不再擴充。
單例的單例
在某些情況下我會使用這種方法來建構唯一執行個體。 即在總單例類中聲明了初始化其他的子單例類,友善了單例的統一擷取和初始化。
擷取某個子單例的執行個體,可以用GameRoot.Instance.dbManager或DBManager.Instance。
作為更高一級的控制器的單例成員或者類變量,同樣可以使該執行個體在整個遊戲中僅存在一份。
其優勢在于擴充性更好,因為我們可以随時添加單例的Controller類,等等。這裡就不再擴充了。
using UnityEngine;
public class GameRoot : MonoBehaviour {
//資料讀取管理類
[HideInInspector]
public DBManager dbManager;
//頁面管理器
[HideInInspector]
public PageManager pageManager;
private static object _lock = new object();
private static GameRoot _instance;
public static GameRoot Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
GameObject go = new GameObject("GameRoot");
_instance = go.AddComponent<GameRoot>();
}
}
return _instance;
}
}
private void Awake()
{
if (_instance == null)
{
_instance = this;
_instance.Initialize();
}
else
{
Destroy(this);
_instance = null;
}
DontDestroyOnLoad(this);
}
void Initialize()
{
dbManager = gameObject.AddComponent<SqlManager>();
dbManager.Init();
pageManager = gameObject.AddComponent<PageManager>();
pageManager.Init();
}
}
DBManager單例類:
public class DBManager : MonoBehaviour {
private static DBManager _instance = null;
public static DBManager Instance
{
get
{
if (_instance == null)
{
_instance = GameRoot.Instance.dbManager;
}
return _instance;
}
}
}