如何正确的使用单例

通常,我们的单例模式都需要有一个静态的函数来获取instance,如:

1
2
3
4
5
6
public static synchronized DataManager getInstance() {
    if (null == sInstance) {
      	sInstance = new DataManager();
    }
    return sInstance;
}

在使用的时候,我们可以:

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

但是,也有这种写法,如:
1
2
3
4
5
6
public static synchronized DataManager getInstance(Context context) {
    if (null == sInstance) {
      	sInstance = new DataManager(context);
    }
    return sInstance;
}

在使用的时候,我们可以:

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

讨论:两种方式看起来都是正确的,我们应该如何取舍呢,哪种方式更好?
支持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源码里面的单例使用方式:
\frameworks\base\core\java\android\view\WindowManagerGlobal.java
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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的地方,让调用者以参数的形式表现出来,如下:
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DigitsKeyListener extends NumberKeyListener {
	private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
    public DigitsKeyListener() {
        this(false, false);
    }

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

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

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

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

        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》中推荐的单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
    }
}