介绍
单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源,这是就要考虑使用单例模式。
实现单例模式主要有如下几个关键点:
- 构造函数不对外开放,一般为 private;
- 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且只有一个,尤其是在多线程环境下;
- 确保单例类对象在反序列化时不会重新构建对象。
单例对象如果持有 Context,那么很容易引发内存泄漏,此时需要注意传递给单例对象的 Context 最好是 Application Context。
示例
饿汉式
在一个应用中,应该只有一个 ImageLoader 实例,这个 ImageLoader 中又含有线程池、缓存系统、网络请求等,很消耗资源。因此,没有理由让它构造多个实例。
上述写法又被称为饿汉式单例模式,在声明静态对象时就已经初始化,符合前面三条关键点,但在反序列化的情况下它们会重新创建对象。
反序列化
我们知道通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的 readResolve() 函数,这个函数可以让开发人员控制对象的反序列化。例如,如果要杜绝上述示例中的单例对象在反序列化时重新生成对象,那么必须加入 readResolve 函数。
也就是在 readResolve 方法中将单例对象返回,而不是重新生成一个新的对象。而对于枚举类,并不存在这个问题,因为即使反序列化它也不会重新生成新的实例。另外有两点需要注意:
- 可序列化类中的字段类型不是 Java 的内置类型,那么该字段类型也需要实现 Serializable 接口;
- 如果调整了可序列化类的内部结构,例如新增、去除某个字段,但没有修改 serialVersionUID,那么会引发 java.io.InvalidClassException 异常或者导致某个属性为 0 或者 null。此时最好的方案是我们直接将 serialVersionUID 设置为 0L,这样即使修改了类的内部结构,我们反序列化不会抛出该异常,只是那些新修改的字段会为 0 或者 null。
其他方式
懒汉式
懒汉模式是声明一个静态变量,并且在用户第一次调用 getInstance 时进行初始化,而上述的恶汉模式是在声明静态对象时就已经初始化。实现如下:
这种模式不能保证在多线程环境下确保单例类的对象有且只有一个,所以我们会添加 synchronized 关键字进行同步。但若是将 synchronized 添加到 getInstance 函数上,会出现每次调用该方法都进行同步的情况,造成不必要的同步开销。代码如下所示:
上述方式不建议使用,但在它的基础上添加双重检查锁定机制(Double Check Lock,DCL)进行优化。
这样一来,DCL 解决了资源消耗、多余同步、线程安全等问题,似乎很完美,但笔者也不推荐这种写法。它还是会在某种情况下出现失效的问题,这个问题被称为双重检查锁定失效,在《Java 并发编程实践》一书的最后谈到了这个问题,并指出这种“优化”是丑陋的,不赞成使用。
静态内部类单例模式
代码如下:
当第一次加载 ImageLoader 类时并不会初始化 sInstance,只有在第一次调用 ImageLoader 的 getInstance 方法时才会导致 sInstance 被初始化。因此,第一次调用 getInstance 方法会导致虚拟机加载 ImageLoaderHolder 类,这种方式不仅能确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。如果有反序列化的情况下,要加入 readResolve 方法,具体代码参考饿汉式反序列章节。
枚举单例
前面讲解的单例模式实现方式不是稍显麻烦就是会在某些情况下出现问题,还有没有更简单的实现方式呢?我们看看下面的实现。
写法简单,而且保证线程安全、序列化与反序列化安全、反射安全。奈何 ANDROID 官方不建议使用 Enums,因为占用内存多(Enums often require more than twice as much memory as static constants.)。关于 Enum 的使用博客。
使用容器实现单例模式
具体代码如下:
在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据 key 获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合。
ANDROID 源码中的单例模式
在 ANDROID 系统中,我们经常会通过 Context 获取系统级别的服务,如 WindowsManagerService、ActivityManagerService 等,更常用的是一个 LayoutInflater 的类。这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过 Context 的 getSystemService(String name) 获取。我们以 LayoutInflater 为例来说明,平时我们使用 LayoutInflater 较为常见的地方是在 ListView 的 getView 方法中。
通常我们使用 LayoutInflater.from(Context) 来获取 LayoutInflater 服务,下面看看 LayoutInflater.from(Context) 的实现。
可以看到 from(Context) 函数内部调用的是 Context 类的 getSystemService(String key) 方法,我们跟踪到 Context 类看到,该类是抽象类。
getView 中使用的 Context 对象的具体实现类是什么呢?其实在 Application、Activity、Service 中都会存在一个 Context 对象,即 Context 的总个数为 Activity 个数 + Service 个数 + 1。而 ListView 通常都是显示在 Activity 中,那么我们就以 Activity 中的 Context 来分析。
我们知道,一个 Activity 的入口是 ActivityThread 的 main 函数,在 main 函数中创建一个新的 ActivityThread 对象,并且启动消息循环(UI 线程),创建新的 Activity、新的 Context 对象,然后将该 Context 对象传递给 Activity。下面我们看看 ActivityThread 源代码。
在 main 方法中,我们创建一个 ActivityThread 对象后,调用了其 attach 函数,并且参数为 false,即非系统应用,会通过 Binder 机制与 ActivityManagerService 通信,并且最终调用 handleLaunchActivity 函数,该函数的实现如下:
通过上面 1~5 注释处的代码分析可以知道,Context 的实现类为 ContextImpl。继续跟踪 ContextImpl 类。
继续跟踪 SystemServiceRegistry 类。
从 SystemServiceRegistry 类的部分代码中可以看到,在虚拟机第一次加载该类时会注册各种 ServiceFetcher,其中就包含了 LayoutInflater Service。将这些服务以键值对的形式储存在一个 HashMap 中,用户使用时只需要根据 key 来获取到对应的 ServiceFetcher,然后通过 ServiceFetcher 对象的 getService 函数来获取具体的服务对象。当第一次获取时,会调用 ServiceFetcher 的 createService 函数创建服务对象,然后将该对象缓存到一个列表中,下次再取时直接从缓存中获取,避免重复创建对象,从而达到单例的效果。这种模式就是前文描述的通过容器实现单例模式。系统核心服务以单例形式存在,减少了资源消耗。