天天看點

如何正确的使用單例

通常,我們的單例模式都需要有一個靜态的函數來擷取instance,如:

public static synchronized DataManager getInstance() {
    if (null == sInstance) {
        sInstance = new DataManager();
    }
    return sInstance;
}
           

在使用的時候,我們可以:

DataManager.getInstnace().initialize(context);
DataManager.getInstnace().doSomething();
           

但是,也有這種寫法,如:

public static synchronized DataManager getInstance(Context context) {
    if (null == sInstance) {
        sInstance = new DataManager(context);
    }
    return sInstance;
} 
           

在使用的時候,我們可以:

讨論:兩種方式看起來都是正确的,我們應該如何取舍呢,哪種方式更好?

支援getInstance(context)的觀點:

1. 在初始化單執行個體時,需要必要的參數來建立對象,DataManager.getInstnace().initialize(context),那就會有一個疑問,每次getInstance()之後,我到底要不要再調用一個函數來初始化?它無法保障DataManager已被正确的初始化。

支援getInstance()的觀點:

1. 單例的設計,一般不需要初始化參數來建立,如果需要一個或者多個參數來初始化,那為什麼要考慮使用單例呢?使用一個普通的類不是更好嗎?單例提供的是一個唯一的通路據點,更多的用來做管理類(XXXManager),維護全局資料,一般它不需要額外的參數來初始化本身。

2. 既然是單例,就不要産生歧義,如果還需要參數的話,不同的參數(不一定是context,可以是其它資料)是否會産生不同的執行個體?

3. 使用getInstance(context),每次傳進去context,都會重新初始化(或重置)我的單例子參數嗎?

4. 使用getInstance(context),在調用層次很深的類裡面,我還得要緩存個context,或者調用函數裡面都得加上context參數?

結論:從上來的觀點來看,個人更加傾向于使用無參的getInstnace()方式。

如果你真的需要參數(可能不止一個)來構造或者初始化你的instance,可以考慮提供initialize方法,并在程式的入口處初始化,這樣,在其它任何地方使用時,不需要考慮初始化問題,看上去似乎有些牽強,不過我們可以找一個具體的使用執行個體。我們看看大名鼎鼎的Android-Universal-Image-Loader的使用方式:

public class UILApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initImageLoader(getApplicationContext());
    }

    public static void initImageLoader(Context context) {
        // This configuration tuning is custom. You can tune every option, you may tune 
        // some of them, or you can create default configuration by
        // ImageLoaderConfiguration.createDefault(this); method.
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
                .threadPriority(Thread.NORM_PRIORITY - )
                .denyCacheImageMultipleSizesInMemory()
                .diskCacheFileNameGenerator(new Md5FileNameGenerator())
                .diskCacheSize( *  * ) // 50 Mb
                .tasksProcessingOrder(QueueProcessingType.LIFO)
                .writeDebugLogs() // Remove for release app
                .build();
        // Initialize ImageLoader with configuration.
        ImageLoader.getInstance().init(config);
    }
}
           

在Android源碼中,有無數單例,我們也一起看看其使用方式:

\frameworks\base\core\java\android\view\WindowManagerGlobal.java

public static WindowManagerGlobal getInstance() {
    synchronized (WindowManagerGlobal.class) {
        if (sDefaultWindowManager == null) {
            sDefaultWindowManager = new WindowManagerGlobal();
        }
        return sDefaultWindowManager;
    }
}
           

像以下類(僅列出幾個),都是采用的上面的方式,這種類型的使用是用得最廣泛的:

\frameworks\base\core\java\android\app\ResourcesManager.java

\frameworks\base\core\java\android\hardware\display\DisplayManagerGlobal.java

\frameworks\base\core\java\android\webkit\CookieManager.java

\frameworks\base\core\java\android\view\accessibility\AccessibilityInteractionClient.java

\frameworks\base\core\java\android\net\http\CertificateChainValidator.java

private static class NoPreloadHolder {
    private static final CertificateChainValidator sInstance = new CertificateChainValidator();
    private static final HostnameVerifier sVerifier = HttpsURLConnection
            .getDefaultHostnameVerifier();
}

public static CertificateChainValidator getInstance() {
    return NoPreloadHolder.sInstance;
}
           

帶參數的getInstance(Context context),也有不少,但數量要小得多:

\frameworks\base\core\java\android\view\accessibility\AccessibilityManager.java

\frameworks\base\core\java\android\hardware\location\GeofenceHardwareImpl.java

\frameworks\base\core\java\android\appwidget\AppWidgetManager.java

Context mContext;

public static AppWidgetManager getInstance(Context context) {
    synchronized (sManagerCache) {
        if (sService == null) {
            IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
            sService = IAppWidgetService.Stub.asInterface(b);
        }

        WeakReference<AppWidgetManager> ref = sManagerCache.get(context);
        AppWidgetManager result = null;
        if (ref != null) {
            result = ref.get();
        }
        if (result == null) {
            result = new AppWidgetManager(context);
            sManagerCache.put(context, new WeakReference<AppWidgetManager>(result));
        }
        return result;
    }
}

private AppWidgetManager(Context context) {
    mContext = context; // 緩存context
    mDisplayMetrics = context.getResources().getDisplayMetrics();
}

public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
    try {
        sService.updateAppWidgetIds(appWidgetIds, views, mContext.getUserId());
    }
    catch (RemoteException e) {
        throw new RuntimeException("system server dead?", e);
    }
}
           

上面的這個例子,每次在擷取單例子時,需要帶上context,用起來麻煩。但也有好處,執行個體在初始化後并将context緩存起來,當内部的函數需要使用context(如updateAppWidget函數),直接使用緩存的mContext即可。

如果讓我來設計這個類,我有兩種改造方式:

  • 不緩存context,在需要使用context的地方,讓調用者以參數的形式表現出來,如下:
public void updateAppWidget(Context context, int[] appWidgetIds, RemoteViews views)
           
  • 緩存context,提供ininalize方法來設定context。但很明顯這有比較大的漏洞,因為在使用其它方法前,我必須要確定initialize方法已被調用過,否則在調用像updateAppWidget這種需要context的方法時,其行為就不正确了。

(寫到這裡,其實我對我之前的觀點也動搖了,因為各有優缺點,是以,還是得需要根據實際情況來做判斷)

在源碼中搜了一下,還有一些另類的getInstance,支援多參數,仔細看一下,發現它就起到了一個緩存的作用,嚴格的來講,已經不是單例的範疇了。

\frameworks\base\core\java\android\text\method\DigitsKeyListener.java

public class DigitsKeyListener extends NumberKeyListener {
    private static DigitsKeyListener[] sInstance = new DigitsKeyListener[];
    public DigitsKeyListener() {
        this(false, false);
    }

    public DigitsKeyListener(boolean sign, boolean decimal) {
        mSign = sign;
        mDecimal = decimal;

        int kind = (sign ? SIGN : ) | (decimal ? DECIMAL : );
        mAccepted = CHARACTERS[kind];
    }

    public static DigitsKeyListener getInstance() {
        return getInstance(false, false);
    }

    public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
        int kind = (sign ? SIGN : ) | (decimal ? DECIMAL : );

        if (sInstance[kind] != null)
            return sInstance[kind];

        sInstance[kind] = new DigitsKeyListener(sign, decimal);
        return sInstance[kind];
    }
}
           

最後,推薦一篇經典的文章,它教你如何構造一個完美的單例。

http://www.iteye.com/topic/575052

另外,再記錄一個《Head First Design Patterns》中推薦的單例:

public class Singleton {
    /**
     * The volatile keyword ensures that multiple threads
     * handle the sInstance variable correctly when it
     * is being initialized to the Singleton instance.
     */
    private volatile static Singleton sInstance;
    private Singleton() {}
    public static Singleton getInstance() {
        if(sInstance == null) {
            synchronized (Singleton.class) {
                if(sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}
           

繼續閱讀