Unity3D 單例模式和靜態類的使用詳解

Unity3D的API提供瞭很多的功能,但是很多流程還是會自己去封裝一下去。當然現在網上也有很多的框架可以去下載使用,但是肯定不會比自己寫的用起來順手。

對於是否需要使用框架的問題上,本人是持肯定態度的,把一些常用方法進行封裝,做成一個功能性的框架,可以很大程度上提高代碼的效率,維護也方便。

對於網絡上很多教程上使用的“遊戲通用MVC框架”,現在看來並不符合MVC這種結構性框架的設計思想:要知道,MVC最初是被設計為Web應用的框架,而遊戲中的很多事件並不是通過用戶點擊UI發生的,View和Controller在遊戲邏輯中的占比一般都少的可憐,而且很多教程上把Model剝離出很多“Manager”模塊,甚至有人把View和Controller合在一起寫瞭UIManager——連MVC的結構都沒瞭,為啥還要稱之為MVC框架呢?

MVC: “人紅是非多。。。。”

目前大部分的遊戲框架——特別是小型項目的遊戲框架——都是把一些數據的特定行為進行瞭一下封裝:生成一個物件,播放一個特效,進行一次隨機事件等。當然也會有一些結構性的設計或者資源管理設計如:UI的回退棧或者回退鏈,場景的載入記錄和切換,下載隊列的管理等。

在Unity的框架設計中,有一個詞會經常見到:單例模式(singleton)。單例模式就是在整個遊戲中隻使用某個類的一個實例,核心的一句話就是public static T Instance;即在類中定義瞭一個靜態的自身實例供外部使用,調用方法時就是:T.Instance.Function()。在本人最初接觸這種設計方式時經常會與靜態類弄混淆,T.Function()。中間差瞭一個靜態Instance,很多時候好像區別不大。。。

在接近兩周左右的時間裡,我一直在糾結於自己正在寫的框架到底應該寫成單例模式的還是靜態模式的,今天剛好對這個問題有瞭一個新的想法:靜態可不可以理解為一種封閉性很強的單例?

首先回想一下靜態的兩個常識:

1、靜態類不能繼承和被繼承!(嚴格點說是隻能繼承System.Object)也就是說你的靜態類不可能去繼承MonoBehaviour,不能實現接口。

2、靜態方法不能使用非靜態成員!如果你大量使用靜態方法,而方法裡又需要用到這個類的成員,那麼你的成員得是靜態成員。

第2點需要註意:如果你想在Unity的編輯器下調整某個參數,那麼這個參數就不能是靜態的(哪怕你自定義EditorWindow去修改這個值也沒用),解決的辦法是通過UnityEngine.ScriptableObject去存放配置(生成*.asset文件),然後在運行中通過LoadAsset去加載,然後再改變靜態成員。至於原因,相信不難理解——你看到的所有Unity組件都是一個個實例,你要通過Unity的編輯器去配置,那麼你就得有一個這樣的可配置實例。

從面向對象上想一下:靜態方法或者靜態類,不需要依賴對象,類是唯一的;單例的靜態實例,一般就是唯一的一個對象(當然也可以有多個)。差別嘛。。。好像也不大。。。

如果這樣考慮沒有錯,那再回頭比較一下兩種方式:

1、靜態(靜態方法或者靜態類),代碼編寫上絆手絆腳,方法調用很方便,運行效率高一丟丟。邏輯面向過程,不能很好地控制加載和銷毀。

2、單例(類的靜態實例),代碼編寫和其他類完全一樣,繼承抽象模版接口都可以,Unity裡也很方便進行參數配置,不過使用麻煩有犯錯的可能性(必須通過實例調用方法),效率不如靜態(但是也不會有很大影響吧)。

如果這些說法太抽象,那我再給出一個常見的問題:如果你的框架有一個SoundManager能夠管理所有的聲音播放,那麼你會怎麼去實現?

(在剛接觸AudioSource這個組件的時候,我想的是每一個聲音都由一個AudioSource去播放。但是後來發現完全沒必要,AudioSource有靜態的PlayClipAtPoint方法去播放臨時3D音效,同時有實例方法PlayOneShot去播放臨時音效(2D和3D取決於當實例的SpatialBlend)。如果沒有特殊的需求,那麼一個AudioSource循環播放背景音樂,上述兩種方法播放遊戲中的特效音頻,這對於大部分遊戲已經足夠瞭。)

那麼問題來瞭:你的SoundManager播放聲音的方法如果是靜態的,那麼AudioSource組件必須在代碼中通過各種方式去獲取(新建組件或者獲取特定GameObject下的組件)——因為保存這個組件的變量必須是靜態的,也就不能通過Unity的編輯器去賦值。如果不去閱讀代碼那麼用戶完全不知道這是一個什麼樣的組件獲取流程,如果我破壞這個流程(同名物體,包含互斥組件等),那麼這個Manager很有可能會出現不可預料的異常。

而繼承MonoBehaviour並RequireComponent(typeof(AudioSource)),怎麼看也比“為瞭靜態而靜態”的代碼要方便健壯的多。

實際上到這裡已經可以基本總結出何時需要使用單例瞭:

1、隻要你的類需要保存其他組件作為變量,那麼就有必要使用單例;

2、隻要你有在Unity編輯器上進行參數配置的需求,那麼就有必要使用單例;

3、隻要你的管理器需要進行加載的順序控制,那麼就有必要使用單例(比如熱更新之後加載ResourcesManager);

當然,這裡都隻是“有必要”,並不是“必須”。兩者區別最大的地方,一個是方便寫,一個是方便用。方便寫的代價是每次調用加個instance,方便用的代價則是放棄瞭面向對象和Unity的“所見即所得”,孰輕孰重,自己抉擇。

另一方面,和“為瞭靜態而靜態”一樣,“為瞭單例而單例”同樣是一個不合理的設計。這樣的解釋仍然是那麼的模糊,那麼,就給自己定義一個最簡單的規則吧——如果你的單例類裡沒有任何需要保存狀態的變量,那麼這個類裡的方法就可以全都是靜態方法,這個類也可以是個靜態類。

補充:從實例出發,瞭解單例模式和靜態塊

就算你沒有用到過其他的設計模式,但是單例模式你肯定接觸過,比如,Spring 中 bean 默認就是單例模式的,所有用到這個 bean 的實例其實都是同一個。

單例模式的使用場景

什麼是單例模式呢,單例模式(Singleton)又叫單態模式,它出現目的是為瞭保證一個類在系統中隻有一個實例,並提供一個訪問它的全局訪問點。從這點可以看出,單例模式的出現是為瞭可以保證系統中一個類隻有一個實例而且該實例又易於外界訪問,從而方便對實例個數的控制並節約系統資源而出現的解決方案。

使用單例模式當然是有原因,有好處的瞭。在下面幾個場景中適合使用單例模式:

1、有頻繁實例化然後銷毀的情況,也就是頻繁的 new 對象,可以考慮單例模式;

2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象;

3、頻繁訪問 IO 資源的對象,例如數據庫連接池或訪問本地文件;

下面舉幾個例子來說明一下:

1、網站在線人數統計;

其實就是全局計數器,也就是說所有用戶在相同的時刻獲取到的在線人數數量都是一致的。要實現這個需求,計數器就要全局唯一,也就正好可以用單例模式來實現。當然這裡不包括分佈式場景,因為計數是存在內存中的,並且還要保證線程安全。下面代碼是一個簡單的計數器實現。

public class Counter {   
    private static class CounterHolder{
        private static final Counter counter = new Counter();
    }
    private Counter(){
        System.out.println("init...");
    }
    public static final Counter getInstance(){
        return CounterHolder.counter;
    }
    private AtomicLong online = new AtomicLong();
    public long getOnline(){
        return online.get();
    }
    public long add(){
        return online.incrementAndGet();
    }
}    

2、配置文件訪問類;

項目中經常需要一些環境相關的配置文件,比如短信通知相關的、郵件相關的。比如 properties 文件,這裡就以讀取一個properties 文件配置為例,如果你使用的 Spring ,可以用 @PropertySource 註解實現,默認就是單例模式。如果不用單例的話,每次都要 new 對象,每次都要重新讀一遍配置文件,很影響性能,如果用單例模式,則隻需要讀取一遍就好瞭。以下是文件訪問單例類簡單實現:

public class SingleProperty {
    private static Properties prop;
    private static class SinglePropertyHolder{
        private static final SingleProperty singleProperty = new SingleProperty();
    }
    /**
    * config.properties 內容是 test.name=kite 
    */
    private SingleProperty(){
        System.out.println("構造函數執行");
        prop = new Properties();
        InputStream stream = SingleProperty.class.getClassLoader()
                .getResourceAsStream("config.properties");
        try {
            prop.load(new InputStreamReader(stream, "utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static SingleProperty getInstance(){
        return SinglePropertyHolder.singleProperty;
    }    
 
    public String getName(){
        return prop.get("test.name").toString();
    }
    public static void main(String[] args){
        SingleProperty singleProperty = SingleProperty.getInstance();
        System.out.println(singleProperty.getName());
    }
}

3、數據庫連接池的實現,也包括線程池。

為什麼要做池化,是因為新建連接很耗時,如果每次新任務來瞭,都新建連接,那對性能的影響實在太大。所以一般的做法是在一個應用內維護一個連接池,這樣當任務進來時,如果有空閑連接,可以直接拿來用,省去瞭初始化的開銷。

所以用單例模式,正好可以實現一個應用內隻有一個線程池的存在,所有需要連接的任務,都要從這個連接池來獲取連接。

如果不使用單例,那麼應用內就會出現多個連接池,那也就沒什麼意義瞭。如果你使用 Spring 的話,並集成瞭例如 druid 或者 c3p0 ,這些成熟開源的數據庫連接池,一般也都是默認以單例模式實現的。

單例模式的實現方法

如果你在書上或者網站上搜索單例模式的實現,一般都會介紹5、6中方式,其中有一些隨著 Java 版本的升高,以及多線程技術的使用變得不那麼實用瞭,這裡就介紹兩種即高效,而且又是線程安全的方式。

1. 靜態內部類方式

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

這種寫法仍然使用 JVM 本身機制保證瞭線程安全問題,由於 SingletonHolder 是私有的,除瞭 getInstance() 方法外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。上面的兩個例子就是用這種方式實現的。

2. 枚舉方式

public enum SingleEnum {
    INSTANCE;
    SingleEnum(){
        System.out.println("構造函數執行");
    }
    public String getName(){
        return "singleEnum";
    }
    public static void main(String[] args){
        SingleEnum singleEnum = SingleEnum.INSTANCE;
        System.out.println(singleEnum.getName());
    }
}

我們可以通過 SingleEnum.INSTANCE 來訪問實例。而且創建枚舉默認就是線程安全的,並且還能防止反序列化導致重新創建新的對象。

靜態塊

什麼是靜態塊呢

1、它是隨著類的加載而執行,隻執行一次,並優先於主函數。具體說,靜態代碼塊是由類調用的。類調用時,先執行靜態代碼塊,然後才執行主函數的;

2、靜態代碼塊其實就是給類初始化的,而構造代碼塊是給對象初始化的;

3、靜態代碼塊中的變量是局部變量,與普通函數中的局部變量性質沒有區別;

4、一個類中可以有多個靜態代碼塊;

他的寫法是這樣的:

static {
        System.out.println("static executed");
    }

來看一下下面這個完整的實例:

public class SingleStatic {
    static {
        System.out.println("static 塊執行中...");
    }
    {
        System.out.println("構造代碼塊 執行中...");
    }
    public SingleStatic(){
        System.out.println("構造函數 執行中");
    }
    public static void main(String[] args){
        System.out.println("main 函數執行中");
        SingleStatic singleStatic = new SingleStatic();
    }
}

他的執行結果是這樣的:

static 塊執行中…

main 函數執行中

構造代碼塊 執行中…

構造函數 執行中

從中可以看出他們的執行順序分別為:

1、靜態代碼塊

2、main 函數

3、構造代碼塊

4、構造函數

利用靜態代碼塊隻在類加載的時候執行,並且隻執行一次這個特性,也可以用來實現單例模式,但是不是懶加載,也就是說每次類加載就會主動觸發實例化。

除此之外,不考慮單例的情況,利用靜態代碼塊的這個特性,可以實現其他的一些功能,例如上面提到的配置文件加載的功能,可以在類加載的時候就讀取配置文件的內容,相當於一個預加載的功能,在使用的時候可以直接拿來就用。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。

推薦閱讀:

    None Found