單例的擴展性討論

在上一篇中,討論了單例的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;
}


最後總結下,在本文中,我們討論了關於單例的一些擴展性應用,包含生命週期,帶參數的構造函數,有限個數的單例,及單例的破壞等話題,歡迎大家來拍磚。

發佈了47 篇原創文章 · 獲贊 8 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章