Android设计模式(一)-单例模式

目录

  1. 定义
  2. 使用场景
  3. UML类图
  4. 实现方式
  5. 饿汉式
  6. 懒汉式
  7. Double Check LockDCL双重检查锁
  8. 静态内部类
  9. 枚举单例
  10. 使用容器实现单例
  11. Android源码中的单例模式
  12. 总结
  13. 优点
  14. 缺点

博客地址
最近在看《Android源码设计模式解析与实战》这本书,发现里面还有对源码的一些分析,之前也没好好看过设计模式,就来做个笔记,跟着看一下。
包括设计模式和一些源码分析。

定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

使用场景

需要确保一个类只有一个实例的场景,避免产生多个对象小号过多的资源,或者是这个类只应该有一个实例。比如创建一个对象要消耗的资源过多,或者要访问IO和数据库等资源.
配置文件,工具类,线程池,缓存,日志对象等。

UML类图


角色介绍:
. Client——高层客户端
. Singleton——单例类

实现单例模式的关键点:

  • 构造函数不对外开放,一般为Private。就是不允许外部通过new Singleton()来获取对象。
  • 通过一个静态方法或枚举返回单例类对象,如getInstance()方法。
  • 确保单例类的对象只有一个,尤其是在多线程的情况下。确保即使在多线程也能实现单例。
  • 确保单例类对象在反序列化时不会重新构建对象。

实现方式

饿汉式

public class Singleton {
    private static final Singleton mSingleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return mSingleton;
    }
}

里面的对象是个静态对象,第一次声明的时候就会实现初始化。外部只能通过getInstance()获取到这个对象,实现单例。

懒汉式

public class Singleton {
    private static  Singleton mSingleton ;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if (mSingleton==null){
            mSingleton=new Singleton();
        }
        return mSingleton;
    }
}

这里的getInstance()方法加上了synchronized关键字,保证了多线程也能实现单例。

优点:
单例只有在使用时才会被实例化,一定程度上节约了资源。
缺点:
(1)单例在第一次加载时要及时进行实例化,反应稍慢
(2)每次调用getInstance()都要进行同步,造成不必要的同步开销。

所以一般不建议用。

Double Check Lock(DCL)双重检查锁

public class Singleton {
    private static  Singleton mSingleton ;
    private Singleton(){}
    public static  Singleton getInstance(){
        if (mSingleton==null){
            synchronized (Singleton.class){
                if (mSingleton==null){
                    mSingleton=new Singleton();
                }
            }
        }
        return mSingleton;
    }
}

这个getInstance方法中对mSingleton进行了两次判空:第一次是为了避免不必要的同步锁;第二层是为了在null的时候创建实例。
DCL失效:
在多线程下,假设A线程执行到mSingleton=new Singleton()的时候,CPU并不是一次性执行完这条语句的,因为这不是一个原子操作(指不会被线程调度机制打断的操作)。
举个例子:执行 Timer timer = new Timer(); 通过字节码文件可以看到这一行代码编译出来是这样的:

         0: new           #2                  // class java/util/Timer
         3: dup
         4: invokespecial #3                  // Method java/util/Timer."<init>":()V
         7: astore_1
         8: return

所以mSingleton=new Singleton()大致做了三件事:
(1)给Singleton的实例分配内存
(2)调用Singleton的构造方法
(3)将mSingleton指向分配的内存空间(这个时候mSingleton才不为空)
由于Java编译器允许处理器乱序执行,所以上面的第二步第三步的执行顺序没法得到保证。执行顺序可能是1-2-3也可能是1-3-2。
当A线程执行顺序是1-3-2的时候,如果执行到了1-3,第2步还没执行的时候,如果B线程判断mSingleton==null的时候就会的发哦FALSE的结果,从而返回一个错误的单例。

优点:
资源利用率高,第一次执行getInstance的时候才会被实例化,效率高。
缺点:
第一册加载反应稍慢,而且有失败的可能,但是概率很小。

这种是用的最多的单例实现方式,大部分情况下都能保证单例。

静态内部类

public class Singleton {
    private Singleton(){}
    public static  Singleton getInstance(){
        return SingletonHolder.mSingleton;
    }
    private static class SingletonHolder{
        private static final Singleton mSingleton = new Singleton();
    }
}

当第一次加载Singleton的时候并不会初始化mSingleton,只有在第一次调用getInstance的时候才会加载SIngletonHolder类。
优点:
不仅能保证线程安全,也能保证单例的唯一性,也延迟了单例的实例化,比较推荐。

枚举单例

public enum Singleton{
    INSTANCE;
    public void doThing(){
        System.out.println(this.hashCode());
    }
}

使用时可以通过Singleton singleton = Singleton.INSTANCE;来获取单例。
优点:
写法简单,而且默认线程安全,任何情况下都是一个单例。

特点:
上面的几种在有一种情况下会单例失效,出现重复创建对象,那就是反序列化。
反序列化的时候会调用一个readResolve()方法重新生成一个实例,所以上面的几种方式要解决这个问题需要加入以下方法,:

public class Singleton {
    private Singleton(){}
    public static  Singleton getInstance(){
        return SingletonHolder.mSingleton;
    }
    private static class SingletonHolder{
        private static final Singleton mSingleton = new Singleton();
    }

    private Object readRedolve() throws ObjectStreamException{
        return SingletonHolder.mSingleton;
    }
}

使用容器实现单例

public class SingletonManager {
    private static Map<String,Objects> objMap = new HashMap<>();
    private SingletonManager(){}
    public static void registerService(String key,Object obj){
        if (!objMap.containsKey(key)){
            objMap.put(key,obj);
        }
    }
    public static Object getService(String key){
        return objMap.get(key);
    }
}

在程序的开始,将许多要单例的对象放到一个容器里,用的时候根据key取得对应的单例对象。

Android源码中的单例模式

源码中的单例模式太多了,甚至有一个专门的单例的抽象类:

package android.util;
public abstract class Singleton<T> {
    private T mInstance;

    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}

我们经常通过context.getSystemService(String name)来获取一些系统服务,如在Activity中获取ActivityManager:

ActivityManager  mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);  

书中举例为LayoutInflater,平时获取方式为LayoutInflater.from(context),看下这个方法:

package android.view;
public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

发现也是通过调用context.getSystemService(String name)获取的。

那么扎个单例是怎么实现的呢?顺着代码往上看吧。。
context.getSystemService(String name)直接点进去的话会进到

package android.content;
public abstract class Context {
...
    public abstract Object getSystemService(@ServiceName @NonNull String name);
...
}

通过分析activity的启动流程可以知道,Context的功能的具体实现是在ContextImpl.java中,看具体实现代码:

package android.app;
class ContextImpl extends Context {
...
    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }
}
...

然后继续SystemServiceRegistry.getSystemService(this, name):

package android.app;
final class SystemServiceRegistry {
    ...
//用来getSystemService的容器,里面存放的是ServiceFetcher<?>
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();
...
//静态代码块,第一次加载时执行,而且只会执行一次,保证了注册的服务的唯一性。
    static {
        registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class,
                new CachedServiceFetcher<AccessibilityManager>() {
            @Override
            public AccessibilityManager createService(ContextImpl ctx) {
                return AccessibilityManager.getInstance(ctx);
            }});
        registerService(Context.DOWNLOAD_SERVICE, DownloadManager.class,
                new CachedServiceFetcher<DownloadManager>() {
            @Override
            public DownloadManager createService(ContextImpl ctx) {
                return new DownloadManager(ctx);
            }});
        ...
//还有很多服务注册
    }
    
  ...
//静态代码块中调用这个方法,把服务名和创建的服务对应放在容器中,实现单例。
      private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }
  ...
  public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
  ...
}

里面还不是直接拿到服务,而是调用了fetcher.getService(ctx)来获取服务。看看ServiceFetcher<?>:

static abstract interface ServiceFetcher<T> {
        T getService(ContextImpl ctx);
    }

这是个接口,看上面的静态代码块里面的方法发现注册服务的时候都是用的CachedServiceFetcher这个类:

static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
        private final int mCacheIndex;

        public CachedServiceFetcher() {
            mCacheIndex = sServiceCacheSize++;
        }

        @Override
        @SuppressWarnings("unchecked")
        public final T getService(ContextImpl ctx) {
//ctx.mServiceCache是获取一个数组:new Object[sServiceCacheSize];
//数组的长度就是构造方法中的那个变量,每注册一个服务,就会new一个对应的CachedServiceFetcher,然后数组长度就+1。第一次获取到这个数组肯定是个空数组
            final Object[] cache = ctx.mServiceCache;
            synchronized (cache) {
                // Fetch or create the service.
                Object service = cache[mCacheIndex];
//第一次获取这个服务的时候,数组是空的 ,所以service == null为TRUE。
                if (service == null) {
//调用注册时实现的createService方法,把生成的具体服务放在数组对应下标中,
//之后就直接从数组中获取了。实现了单例。
                    service = createService(ctx);
                    cache[mCacheIndex] = service;
                }
                return (T)service;
            }
        }
      //  在静态代码块中实现
        public abstract T createService(ContextImpl ctx);
    }

里面有个抽象方法,需要实例化的时候实现。在静态代码块中的方法都实现了这个createService(ContextImpl ctx)方法,并且返回了对应的服务。
附上部分注册服务的截图,截不下:


总结

优点:

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁创建,或者创建或销毁时性能无法优化,单例模式的优势就很明显了。
  • 由于只生成一个实例,减少了系统性能开销。
  • 可以避免对资源的多重占用,如文件操作等。
  • 单例模式可以设置为全局的访问点,优化和共享资源访问。

缺点

  • 单例模式一般没有接口,拓展很困难,基本都要修改源代码。
  • 在Android中,如果单例模式持有Activity的Context,容易产生内存泄漏。所以尽量用ApplicationContext。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容