Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android框架設計模式(五)——Singleton Method

Android框架設計模式(五)——Singleton Method

編輯:關於Android編程

一、單例模式介紹


什麼是單例模式

單例模式就是在整個全局中(無論是單線程還是多線程),該對象只存在一個實例,而且只應該存在一個實例,沒有副本(副本的制作需要花時間和空間資源)。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中一個類只有一個實例而且該實例易於外界訪問,從而方便對實例個數的控制並節約系統資源。如果希望在系統中某個類的對象只能存在一個,同時該對象需要協調系統整體的行為,單例模式是最好的解決方案。單例模式相當於只有一個入口的系統,使得所有想要獲取該系統資源的對象都要經過該入口。

不管是在單線程應用還是多線程並發的應用,單例模式的使用都是一樣的,只是在多線程並發的情況下,對於單例模式的實現方式需要加同步管理機制。

單例模式UML圖


這裡寫圖片描述


單例模式應用場景

確保某個類有且只有一個對象,避免產生多個對象消耗過多的資源(時間、空間),或者某種類型的對象只應該有一個的特殊情況。同時,使用單例模式能夠體現共享和同步的思想,因為單例就是全局的意思,全局即共享,它需要協調系統的整體行為。因此,使用單例模式往往是與同步分不開的。

總的來說有下面三個需求的都可以使用單例模式:
(1)需要保證一致性,即間斷性的操作時,每一次的操作結果會保留,下一次繼續從上一次的間斷點開始(I/O流,數據庫鏈接、緩沖區等)。

(2)在邏輯上只能有一個實例(比如:某個具體的動物是獨一無二的)

(3)頻繁的創建和使用,而且創建消耗的資源較大。(I/O流、數據庫鏈接)

例如:

I/O流操作
對於I/O流操作來說,創建過程復雜而且消耗很多資源。輸入流只是一個字符流通渠道,對於一個文件流來說多個對象並沒有實際意義,而且它需要保證一致性,多個對象只會加重冗余和開銷。比如說:創建某一個文件的輸入流,那麼該輸入流的任務就是將文件從外存讀入內存當中而已,如果已經讀到了一般,然後你關閉輸入流;然後重新打開輸入流,這時候如果獲取的是一個新的輸入流的話,因為是另外的一個輸入流對象,那麼他的指針和其他信息就與之前不一致,那麼一切又將重新開始。如果你要讓他保持一致性,那麼要額外花費更多的開銷在將之前的信息復制到現有的對象上,因此倒不如直接就是單例模式。

數據庫連接
對於數據庫來說,一個應用甚至多個應用都可以只對應一個數據庫。同時,對於數據庫訪問來說,需要有一個連接池管理,以及同步管理來限制鏈接的數量和數據的同步。如果重新創建了一個數據庫連接對象,那麼當前創建的對象鏈接信息將與之前的信息不一致,造成程序隱患或者崩潰(如果要使得一致,那麼又要重新對該對象進行資源初始化)。同樣還是要保證一致性,因此單例在這裡正好可以限制程序實現一個連接池,以及同步的機制,節省資源和時間。

上面的兩種對象的創建都需要消耗內存和時間,而且由於他們需要保證前後一致性,因此也只應該有一個實例。


二、單例模式的實現


實現單例模式主要有如下幾個關鍵點:
(1)構造函數不對外開放,一般為private
(2)通過一個靜態方法或者枚舉返回單例對象
(3)確保單例類的對象有且只有一個,尤其是在多線程的環境下
(4)確保單例類對象在反序列化時不會重新構建對象

上述關鍵點中,(1)、(2)兩點容易實現,(3)、(4)比較麻煩,許多單例模式的實現都是在單線程下的,多線程下的單例模式就需要加上同步機制,而當需要把對象刻到磁盤上存放時,就會牽扯到反序列化的問題。因此單例模式在(3)、(4)的應用情況下需要特別處理。下面介紹幾種實現單例模式的方式,他們有些是線程不安全的,有些是線程安全的;對於反序列化重構對象,只有枚舉可以防止。

注意:對於反序列化,系統提供一個方法讓程序猿控制對象的反序列化。因此,對於反序列化我們可以自己覆蓋該方法進行處理。

幾種實現方式


懶漢1(線程不安全)

public class Singleton {  
 private static Singleton instance;  
     private Singleton (){}   
     public static Singleton getInstance() {  
     if (instance == null) {  
         instance = new Singleton();  
     }  
     return instance;  
      }  
 }  

該方法在多線程環境下無法實現單例,無法防止反序列化重新構建對象。
優點:最為簡單;之所以稱為懶漢,是因為它把單例初始化延遲到第一次調用
getInstance方法上。
缺點:線程不安全


懶漢2(線程安全)

 public class Singleton {  
      private static Singleton instance;  
      private Singleton (){}
      public static synchronized Singleton getInstance() {  
      if (instance == null) {  
          instance = new Singleton();  
      }  
      return instance;  
      }  
 }  

使用synchronized關鍵字修飾getInstance方法,這樣可以保證一般情況下單例對象的唯一性,但是會產生另一個問題:即使已經創建了單例,每次調用getInstance方法還是會進行同步(同步資源競爭處理)。這樣浪費了不必要的資源,同時也將單例模式節省資源的性能降低。這是懶漢模式(線程安全)的一大弊病。

優點:線程安全
缺點:99%的同步是多與的


雙重校檢鎖(線程安全)

public class Singleton1 {
    private static Singleton1 instance = null;
    private static Object synObj = new Object();
    private Singleton1(){

    }
    public static Singleton1 getInstance(){
        if (instance == null) {//先判斷有沒有實例化
            synchronized(synObj){//如果沒有被實例化就請求鎖
                if (instance == null) {//得到鎖之後,再次判斷是否已經被先前獲得鎖的對象實例化
                    return instance = new Singleton1();
                }
            }
        }
        return instance;
    }
}

雙重校檢鎖,把懶漢模式的弊病避免了,它首先判斷是否已經有實例,然後再去競爭鎖,競爭到鎖了之後再一次判斷,這樣就可以避免在對象已經被實例化的情況下參與鎖的競爭。
優點:單例唯一,線程安全
缺點:JDK1.6以上;第一次加載比較慢;偶爾會失敗(雙重檢查鎖定失效)


餓漢(靜態成員變量)

public class Singleton {  
      private static Singleton instance = new Singleton();  
      private Singleton (){}
      public static Singleton getInstance() {  
      return instance;  
     }  
 }

使用靜態成員變量只創建一次的特性實現單例模式,靜態成員變量只創建一次這個特性是由classLoader管理,它可以保證該成員在整個全局只被初始化一次。

優點:代碼簡潔,不需要同步鎖

缺點:之所以稱為餓漢,是因為只要編譯器一看到該類就會初始化單例,無法達到
延遲加載的目的。


靜態內部類

package designmodle.singleton;

/**
 * @author David
 *
 */
public class Singleton3 {
   private static class SingletonHolder{
       private static final Singleton3 instance = new Singleton3();
   }
   private Singleton3(){}

   public static Singleton3 getInstance(){
       return SingletonHolder.instance;
   }
}

與餓漢(靜態成員變量)方法同樣是使用classLoader機制,比餓漢好的地方在於它能夠延遲加載,當第一次加載Singleton類時並不會初始化instance,只有在第一次調用getInstance方法時才會導致SingleHolder被加載,同時instance被初始化。

優點:線程安全,對象唯一性,延遲實例化

缺點:暫無


枚舉(線程安全,且防反序列化)

public enum Singleton {  
     INSTANCE;  
     public void whateverMethod() {  
     }  
 }  

一看就知道,如此簡單!枚舉是最簡單的實現單例模式的方法,而且最重要的是枚舉默認是線程安全的,同時,還可以防止反序列化!
枚舉的這種方式很少有人使用,但是相當之簡單!而且完全符合單例模式的全部關鍵要素。
優點:實例唯一、線程安全、防止反序列化重構、簡單,簡單,簡單!

缺點:JDK1.5以上


容器實現單例模式


如果程序中有許多單例類別,那麼可能會需要一個容器類別進行管理,因此我們也可以通過容器進行實現多種類單例模式,同時使用了容器就能夠使得獲得單例接口統一,降低耦合度。

public class SingletonManager{
   private static Map objMap = new HashMap();
   private SingletonManager(){}
   public static void registerService(String key,Object instance){
      //判斷是否包含了該單列對象,若沒有包含則添加
      if(!objMap.containsKey(key)){
         objMap.put(key,instance);
      }
   }

   public static Object getService(String key){
      return objMap.get(key);
   }
}

這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶成本,也對用戶隱藏了獲取單例的具體實現(不用知道單例的類名和獲取方法),降低了使用者與被使用單例類的耦合度。

優點:單例唯一,線程安全(指的是多線程情況下獲得的是同樣的單例,HashMap本身並不是線程安全的),單例類別管理,降低單例類別與用戶之間的耦合。

缺點:暫無


如何防止反序列化重建對象

如何防止反序列化帶來的問題,其實很簡單。反序列化可以通過特殊的途徑創建一個類的新實例而不管該類的構造函數的可見性。但是系統給我們提供了一個很特別的hook方法,是專門讓開發人員控制對象的反序列化,該方法就是:readResolve(),我們可以在這個方法內部杜絕單例對象在背反序列化時重新生成對象。

加入方式很簡單,舉靜態內部類的方式為例:

public class Singleton3 implements Serializable {
   private static class SingletonHolder{
       private static final Singleton3 instance = new Singleton3();
   }
   private Singleton3(){}

   public static Singleton3 getInstance(){
       return SingletonHolder.instance;
   }

   //添加這個hook函數,那麼系統在反序列化的過程中就會通過該Hook方法得到原有的單例
   //而不是重新創建一個單例。
   private Object readResolve() throws ObjectStreamException{
       return SingletonHolder.instance;
   }
}

實現方式小結

無論哪種形式實現單例模式,它們的核心原理都是將構造函數私有化,並且通過靜態方法獲取一個唯一的實例,在這個獲取的過程中保證線程安全、防止反序列化導致的對象重新生成等問題,選擇哪種方式取決於項目本身。
(1)是否是復雜的並發環境
(2)JDK版本是否過低
(3)單例對象的資源消耗等


三、Android中單例模式范例


在Android系統中,我們經常會通過Context獲取系統級別的服務,如WindowsManagerService、ActivityManagerService等,更加常用的是一個LayoutInflater的類,這些服務會在何時的時候以單例的形式注冊在系統中,在我們需要的時候就通過Context的getSystemService(String name)獲取,我們會看到Android中是使用容器的方式來多種服務的單例管理。我們以LayoutInflater為例來說明,平常我們使用LayoutInflater較為常見的地方是在ListView的getView()策略方法中:

LayoutInflater

@Override
public View getView(int position,View convertView, ViewGroup parent){
   ViewHolder holder = null;
   if(null == convertView){
       holder = new ViewHolder();
       convertView = LayoutInflater.from(mContext).inflate(mLayoutId,null);
       //代碼省略
   }else{
       //代碼省略
   }
   //代碼省略
   return convertView;
}

我們是通過使用LayoutInflater.from(Context)來獲取LayoutInflater服務的,下面看看LayoutInflater.from(Context)的實現:

public static LayoutInflater from(Context context){
  //獲取系統的LayoutInflater服務
   LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   if(LayoutInflater == null){
      throw new AssertionError("LayoutInflater not found.");
   }
   return LayoutInflater;
}

我們從上面的代碼可以看到,from(Context)方法是調用Context類的getSystemService(String key)方法。Context是一個抽象類別,那在Android中就一定還有它的實現類來操作Context的功能,在Application、Activity、Service中都會有一個Context對象,即Context的總個數為:Activity個數+Service個數+1(Application)。而ListView都是顯示在Activity中的,因此我們可以查閱Activity的入口ActivityThread的main函數,對Activity的創建進行跟蹤,可以發現最終在handleBindApplication方法中函數中創建了一個ContextImpl對象,而該對象就是Context的實現類。

public Application makeApplication(boolean forceDefaultAppClass,  
        Instrumentation instrumentation) {  
    if (mApplication != null) {  
        return mApplication;  
    }  

    Application app = null;  

    String appClass = mApplicationInfo.className;  
    if (forceDefaultAppClass || (appClass == null)) {  
        appClass = "android.app.Application";  
    }  

    try {  
        java.lang.ClassLoader cl = getClassLoader(); 
        //創建了ContextImpl類 
        ContextImpl appContext = new ContextImpl();  
        appContext.init(this, null, mActivityThread);  
        app = mActivityThread.mInstrumentation.newApplication(  
                cl, appClass, appContext);  
        appContext.setOuterContext(app);  
    } catch (Exception e) {  
        if (!mActivityThread.mInstrumentation.onException(app, e)) {  
            throw new RuntimeException(  
                "Unable to instantiate application " + appClass  
                + ": " + e.toString(), e);  
        }  
    }  
    ...  
}  

ContextImpl類:

class ContextImpl extends Context{
   //代碼省略   
   //ServiceFecher抓取器,通過getService獲取各類服務對象
   static class ServiceFecher{
       //當前服務在容器中所處的位置,便於下面同步塊中獲取對應的服務
       int mContextCacheIndex = -1;
       //獲取系統服務
       public Object getService(ContextImpl ctx){
         ArrayList

從ContextImpl的部分代碼中可以看到,ContextImpl是采用容的方式實現的單例模式。在虛擬機第一次加載該類時會注冊各種ServiceFetcher,其中就包含了LayoutInflater Service。將這些服務以鍵值對的形式存儲在一個HashMap中,用戶使用時只需要根據key來獲取對應的ServiceFetcher,然後通過ServiceFetcher對象的getService函數來獲取具體的服務對象,當第一次獲取時,會調用它的hook方法createService函數創建服務對象,然後將該對象緩存到一個列表中,下次再取的時候直接從緩存中獲取,避免重復創建對象,從而達到單列的效果。這種模式下,系統的核心服務以單例的形式存在,減少了資源的消耗。


四、總結


單例模式是應用最廣的模式之一,也是許多人最初接觸並使用的設計模式。在我們的系統只需要一個全局的對象時,我們就可以使用它來節省系統資源並達到協調系統整體行為的目的。在應用這個模式的時候,要注意所說的4個關鍵點:構造函數私有化、一個單例、多線程安全、防止反序列化重建對象。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved