在上一篇中,討論了單例的4種基本形態,這次我們來探討單例的變形。
1.有限個數的單例形式。即這個對象可能有多個,從這個角度上說,它其實不屬於單例,但實現方式確是以單例爲基礎的。它通常是以帶參數的getInstance(或其變型)存在。
public class MultiInstanceDemo { private String mType; private MultiInstanceDemo(String type) { mType = type; } //單例 //每種類型限制只能有一個實例 private static final HashMap<String, MultiInstanceDemo> mInstanceMap = new HashMap<String, MultiInstanceDemo>(); public static MultiInstanceDemo getInstance(String type) { MultiInstanceDemo instance = mInstanceMap.get(type); if( instance == null) { synchronized (MultiInstanceDemo.class) { instance = mInstanceMap.get(type); if( instance == null) { //double check instance = new MultiInstanceDemo(type); mInstanceMap.put(type,instance); } } } return instance; } }
從上面的代碼中可以看到,getInstance是帶了一個參數,這個參數其實是做爲Key來保證對於相同的Key,只創建同一個對象。從另一個角度來說,這個getInstance實際上是被做工廠方法來使用。
在Android中,最常用到的一個Context.getSystemService(String), 其實就是用了這種結構。
在ContextImpl.java中,我們看到有這麼一個結構。
private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP = new HashMap<String, ServiceFetcher>();
private static int sNextPerContextServiceCacheIndex = 0; private static void registerService(String serviceName, ServiceFetcher fetcher) { if (!(fetcher instanceof StaticServiceFetcher)) { fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++; } SYSTEM_SERVICE_MAP.put(serviceName, fetcher); }
registerService(ACCESSIBILITY_SERVICE, new ServiceFetcher() { public Object getService(ContextImpl ctx) { return AccessibilityManager.getInstance(ctx); }}); registerService(CAPTIONING_SERVICE, new ServiceFetcher() { public Object getService(ContextImpl ctx) { return new CaptioningManager(ctx); }}); registerService(ACCOUNT_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(ACCOUNT_SERVICE); IAccountManager service = IAccountManager.Stub.asInterface(b); return new AccountManager(ctx, service); }}); registerService(ACTIVITY_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler()); }});
@Override public Object getSystemService(String name) { ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); return fetcher == null ? null : fetcher.getService(this); }
2. 單例的生命週期討論
一般說來,單例的對象是存在靜態變量中的,除非是進程被殺死,這個單例就會永遠存在。那麼,有沒有必要寫一個Destory方法,把這個instance設爲null, 以節約內存呢?
我的意見是這種做會破壞單例的唯一性。
因爲你不知道在程序的哪個角落保留了對當前這個單例的引用。一旦在單例對象(mInstance=null)設爲null後,下次當別人調用getInstance時,會又重新生成單例,此時內存中其實是同時存在兩個及以上的對象,這就破壞了單例的唯一性。
因此,不建議去釋放單例所佔據的內存,故請謹慎使用單例。
那問題來了,如果我想提高內存的使用效率,只想創建一個短命的單例對象怎麼辦?
通常情況下,那就不能以常規的方法來創建或是在靜態變量中存放單例的實例。需要把這個“單例”的對象作爲另一個帶有生命週期的對象的成員。這裏寫了一個例子來探討這種情況。
SingleMethod.java,這是一個接口,表明單例中用到的所有公開的方法
/** * Created by Rex on 4/12/2015. */ //很折騰的一個接口,包含單例中所有public的方法 public interface SingleMethod { public void methodA(); public void methodB(); }
ShortSingle.java,這個是真正的單例,但對外不可見,無法直接訪問
/** * Created by Rex on 4/12/2015. */ //真正的單例在這兒了 class ShortSingle implements SingleMethod { /* package */ ShortSingle() { } @Override public void methodA() { } @Override public void methodB() { } }
singleVistor.java,用來訪問單例的方法,提供給外部使用,這個對象可創建多次,可保留多個引用,但最關鍵的是傳入的single必須是真正的單例。
/** * Created by Rex on 4/12/2015. */ //單例的訪問者,此對象允許存在多個 public class SingleVistor implements SingleMethod { private ShortSingle mSingle; /* package */ SingleVistor(ShortSingle single) { mSingle = single; } @Override public void methodA() { final ShortSingle shortSingle = mSingle; if( shortSingle != null) { shortSingle.methodA(); } } @Override public void methodB() { final ShortSingle shortSingle = mSingle; if( shortSingle != null) { shortSingle.methodB(); } } /* package */ void destory() { mSingle = null; } }
LifeCycleObject.java 這是一個具有生命週期的類,init是開始, destory是結束。
/** * Created by Rex on 4/11/2015. */ public class LifeCycleObject { //真正的單例,我們要確保這個對象只能有一份 private static ShortSingle mSingle; //單例對象的訪問者,外面通過這個對象來使用單例 private static SingleVistor mSingleVistor; //既然是短命的對象,自然就有開始與結束 public static void init() { synchronized (LifeCycleObject.class) { if( mSingle == null) { mSingle = new ShortSingle(); } if( mSingleVistor == null) { mSingleVistor = new SingleVistor(mSingle); } } } //銷燬了對象 public static void destory() { synchronized (LifeCycleObject.class) { mSingle = null; if( mSingleVistor != null) { mSingleVistor.destory(); mSingleVistor = null; } } } //返回一個包含單例方法一個接口,外面使用就不管它到底是什麼對象,由於有了生命週期,則是有可能爲null的 public static SingleMethod getSingleObject() { return mSingleVistor; } //for test only static SingleMethod getRealSingle() { return mSingle; } }
在LifeCycleObject的生命週期中,從第一次調用init到destory之間,中間不管init調用多次,或是創建了多少個LifeCycleObject的對象,其中ShortSingle這個真正的單例只生一個。而且,這個單例本身對外面是隱藏的,外面無法獲取這個單例的引用,外面訪問的是單例的一個接口,即SingleMethod, 的另一個實現SingleVistor, 同樣,這個SingleVistor的引用保持並不破壞單例的性質。因爲就算保留了引用,但在destroy中,這個引用會置空。這樣就達到了嚴格控制單例對象的目的。
在destroy之後,則單例在內存中的引用設置爲null,所佔用內存就釋放了。到一下次的重新init, 此時單例會創建新的對象,但始終保持內存中最多隻有一份單例對象。
這種設計優勢是能較好的控制單例的生命週期,但使用成本較高,維護不方便,每次修改單例,需要修改三個類,而且結構複雜,讀起代碼比較痛苦。因此,如果不是對內存的要求特別苛刻,不推薦使用。
3.另一種帶參數的單例,是需要初始化的。這種單例用起來也需要小心。最常見的場景是在Android中,有時候代碼如網絡,數據庫等模塊,需要一個applicationcontext作爲參數。這種怎麼處理呢,建議的寫法是把參數單獨提出來,做一個init或是setup的方法,然後在必要時才創建單例,這樣對使用者友好,而且內存使用效率也不錯。
public class SingletonWithParam { private volatile static SingletonWithParam mInstance; private static Object mParam; private Object mBigObject; //lazy init private SingletonWithParam(Object param) { //create other object with param mBigObject = new Object(); } //initialize when the app start public static void setup(Object param) { mParam = param; } public static SingletonWithParam getInstance() { if( mInstance == null) { synchronized (SingletonWithParam.class) { if( mInstance == null) { //double check mInstance = new SingletonWithParam(mParam); } } } return mInstance; } }
4.單例的破壞
在通常情況下,我們寫的單例是能正常工作的。但這世界總有一些例外,請看下面的代碼。
這是一個我們常寫的單例模式
public class SimpleInstance { private volatile static SimpleInstance mInstance; private SimpleInstance() { } public static SimpleInstance getInstance() { if( mInstance == null) { synchronized (SingletonWithParam.class) { if( mInstance == null) { //double check mInstance = new SimpleInstance(); } } } return mInstance; } }
這是一個單元測試
public class SimpleInstanceTest { @Test public void instanceTest() { SimpleInstance simpleInstance1 = SimpleInstance.getInstance(); SimpleInstance simpleInstance2 = null; try { Constructor<SimpleInstance> constructor = SimpleInstance.class.getDeclaredConstructor(); constructor.setAccessible(true); simpleInstance2 = constructor.newInstance(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } assertNotNull(simpleInstance2); assertNotEquals(simpleInstance1,simpleInstance2); } }
而測試的結果是simpleInstance1並不等於simpleInstance2, 也就是說,對於通過反射的方式,是可以破壞單例的性質的。因此,通常情況下,我們的代碼是防君子不防小人。那有辦法防止嗎? 額,有一種招術叫防禦性編碼,我們可以使用一個小技巧。
在構造函數加上一個assert語句。
private SimpleInstance() { assert(mInstance == null); }
這樣,想反射我的構造函數?沒門。
除了反射,還有其他方式破壞嗎?有,單例的序列化,網上有關這個的討論很多,這裏也不浪費篇幅了。直接上結論吧,爲了防止單例的性質不被破壞,需要加上這麼一個方法:
private Object readResolve() { return mInstance; }
最後總結下,在本文中,我們討論了關於單例的一些擴展性應用,包含生命週期,帶參數的構造函數,有限個數的單例,及單例的破壞等話題,歡迎大家來拍磚。