1、介绍
单例模式是设计模式的经典之一,它确保某一个类只有一个实例,并提供一个全局的访问点来获取该实例。对于Android开发,单例模式的应用相当广泛,例如网络请求、数据库操作等场景,常常需要全局的管理或是保持状态的一致性。在本文中,Kotlin开发的角度探讨单例模式,了解其用法、优势以及Kotlin为我们提供的优雅实现方式。
2、目的
目的在介绍中其实已经概括完了,可以稍微打开一点
- 受控的资源访问:单例可以避免多个实例竞争使用同一资源,例如写日志文件或共享资源。
- 节省系统资源:如果一个对象需要频繁地被创建、销毁,但每次只用一个,这样会浪费系统资源。使用单例可以避免这种浪费。
- 全局状态:如果需要一个全局的状态,单例是一个非常好的选择。
3、优雅的Kotlin实现
-
饿汉式:在类加载时就创建单例(我好饿啊,开始就想要)
这种方式的优点是实现简单,但可能会浪费资源。
object Singleton { // 对象的初始化逻辑 }
-
懒汉式(双重校验锁式) :在第一次使用时创建单例,需要确保线程安全,通过lazy实现(懒,用的时候在说)
class SingletonLazy private constructor() { companion object { val instance: SingletonLazy by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { SingletonLazy() } } }
-
懒汉式(线程安全) :通过 @Synchronized实现
class SingletonSync private constructor() { companion object { private var instance: SingletonSync? = null get() { if (field == null) { field = SingletonSync() } return field } @Synchronized fun get(): SingletonSync { return instance!! } } }
-
枚举式:使用枚举来实现单例(又又又是枚举)
Effective Java书中被推荐为最佳的单例实现方法,因为它能自动支持序列化机制,并且通过枚举类可以防止反射攻击(Kotlin说嘻嘻嘻)。
enum class SingletonEnum { INSTANCE; fun doSomething() { // ... } }
-
带参数:有时候,可能需要一个参数
class SingletonParam private constructor(val parameter: String) { companion object { @Volatile private var instance: SingletonParam? = null fun getInstance(parameter: String) = instance ?: synchronized(this) { instance ?: SingletonParam(parameter).also { instance = it } } } }
每次都这么写会有点麻烦,可以稍微封装下
open class SingletonHolder<out T, in A>(private val block: (A) -> T) { @Volatile private var instance: T? = null fun getInstance(param: A): T = instance ?: synchronized(this) { instance ?: block(param).apply { instance = this } } }
使用
class SingletonTest private constructor(str: String) { companion object : SingletonHolder<SingletonTest, String>(::SingletonTest) } // SingletonTest.getInstance("嘻嘻嘻")
5、注意事项
单例模式虽然看起来简单,但在实际应用中有许多细节需要注意
- 线程安全:在多线程环境下,需要确保单例的创建是线程安全的。否则,可能会出现多个线程几乎同时创建多个实例的情况。
- 延迟初始化:在某些情况下,可能希望在单例第一次使用时才创建它,以节省资源和初始化时间。但请注意,延迟初始化可能会增加获取单例的复杂性,尤其是在确保线程安全性时。
- 防止反射攻击:通过反射,攻击者可能能够调用私有构造函数创建新的实例。为了防止这种情况,可以在构造函数中添加逻辑来检查实例是否已存在,如果已存在则抛出异常。
- 防止通过序列化和反序列化创建新的实例:如果单例类是可序列化的,那么反序列化时可能会创建一个新的实例。为了避免这种情况,可以在单例类中添加
readResolve()
方法,使其返回已有的单例实例。 - 确保单例的唯一性:如果应用程序有多个类加载器,那么每个类加载器可能会加载一个单例类的不同实例。确保在这种情况下,单例仍然是唯一的。
可以分析下,在以上示例中,都解决了哪些问题。
其二,在选择和使用单例模式时我们需要考虑到:
- 全局状态的管理:单例通常用于管理全局状态,但过多的全局状态可能会导致代码难以维护和理解。考虑是否真的需要全局状态,或者是否可以使用其他方法来管理状态。
- 单元测试的难度:单例模式可能会使单元测试变得更加困难,因为它们可能在测试之间保留状态。考虑使用依赖注入或其他技术来解决这个问题。
- 考虑使用作用域单例:在某些情况下,可能不需要一个全局的单例,而只是在某个特定的作用域或上下文中需要一个单例(例如,每个请求一个单例)。在这些情况下,可以考虑使用作用域单例而不是全局单例。
- 考虑可维护性和扩展性:单例类通常不应该做太多事情。如果发现单例类的功能不断增加,可能是时候考虑将其分解为更小的部分。
- 释放资源:如果单例持有重要的资源(如文件句柄、数据库连接等),确保在不再需要时释放这些资源。
其三,如果在Android中使用,需要特别注意内存泄漏:如果单例持有一个对Activity、View或其他具有生命周期的对象的长时间引用,可能会导致内存泄漏。当那个Activity应该被垃圾回收时,由于单例的引用,它不会被销毁,从而导致内存泄漏。
6、结尾
Android中使用单例模式的地方是十分多的,例如:Context.getSystemService()
、Application 、Retrofit、Room Database、Dagger/Hilt、EventBus、Shared Preferences等等等,都可以自行学习~