单例的扩展性讨论

在上一篇中,讨论了单例的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;
}


最后总结下,在本文中,我们讨论了关于单例的一些扩展性应用,包含生命周期,带参数的构造函数,有限个数的单例,及单例的破坏等话题,欢迎大家来拍砖。

单例 反射
您的回应...

相关话题

查看全部

也许你感兴趣

换一批

热门标签

更多