如何正确的使用单例
通常,我们的单例模式都需要有一个静态的函数来获取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; } } |