Kotlin设计模式-单例

250 阅读5分钟

1、介绍

单例模式是设计模式的经典之一,它确保某一个类只有一个实例,并提供一个全局的访问点来获取该实例。对于Android开发,单例模式的应用相当广泛,例如网络请求、数据库操作等场景,常常需要全局的管理或是保持状态的一致性。在本文中,Kotlin开发的角度探讨单例模式,了解其用法、优势以及Kotlin为我们提供的优雅实现方式。

2、目的

目的在介绍中其实已经概括完了,可以稍微打开一点

  1. 受控的资源访问:单例可以避免多个实例竞争使用同一资源,例如写日志文件或共享资源。
  2. 节省系统资源:如果一个对象需要频繁地被创建、销毁,但每次只用一个,这样会浪费系统资源。使用单例可以避免这种浪费。
  3. 全局状态:如果需要一个全局的状态,单例是一个非常好的选择。

3、优雅的Kotlin实现

  1. 饿汉式:在类加载时就创建单例(我好饿啊,开始就想要)

    这种方式的优点是实现简单,但可能会浪费资源。

     object Singleton {    
       // 对象的初始化逻辑 
     }
    
  2. 懒汉式(双重校验锁式) :在第一次使用时创建单例,需要确保线程安全,通过lazy实现(懒,用的时候在说)

     class SingletonLazy private constructor() {
         companion object {
             val instance: SingletonLazy by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
                 SingletonLazy()
             }
         }
     }
    
  3. 懒汉式(线程安全) :通过 @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!!
             }
         }
     }
    
  4. 枚举式:使用枚举来实现单例(又又又是枚举)

    Effective Java书中被推荐为最佳的单例实现方法,因为它能自动支持序列化机制,并且通过枚举类可以防止反射攻击(Kotlin说嘻嘻嘻)。

     enum class SingletonEnum {
         INSTANCE;
     ​
         fun doSomething() {
             // ...
         }
     }
    
  5. 带参数:有时候,可能需要一个参数

     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、注意事项

单例模式虽然看起来简单,但在实际应用中有许多细节需要注意

  1. 线程安全:在多线程环境下,需要确保单例的创建是线程安全的。否则,可能会出现多个线程几乎同时创建多个实例的情况。
  2. 延迟初始化:在某些情况下,可能希望在单例第一次使用时才创建它,以节省资源和初始化时间。但请注意,延迟初始化可能会增加获取单例的复杂性,尤其是在确保线程安全性时。
  3. 防止反射攻击:通过反射,攻击者可能能够调用私有构造函数创建新的实例。为了防止这种情况,可以在构造函数中添加逻辑来检查实例是否已存在,如果已存在则抛出异常。
  4. 防止通过序列化和反序列化创建新的实例:如果单例类是可序列化的,那么反序列化时可能会创建一个新的实例。为了避免这种情况,可以在单例类中添加readResolve()方法,使其返回已有的单例实例。
  5. 确保单例的唯一性:如果应用程序有多个类加载器,那么每个类加载器可能会加载一个单例类的不同实例。确保在这种情况下,单例仍然是唯一的。

可以分析下,在以上示例中,都解决了哪些问题。

其二,在选择和使用单例模式时我们需要考虑到:

  1. 全局状态的管理:单例通常用于管理全局状态,但过多的全局状态可能会导致代码难以维护和理解。考虑是否真的需要全局状态,或者是否可以使用其他方法来管理状态。
  2. 单元测试的难度:单例模式可能会使单元测试变得更加困难,因为它们可能在测试之间保留状态。考虑使用依赖注入或其他技术来解决这个问题。
  3. 考虑使用作用域单例:在某些情况下,可能不需要一个全局的单例,而只是在某个特定的作用域或上下文中需要一个单例(例如,每个请求一个单例)。在这些情况下,可以考虑使用作用域单例而不是全局单例。
  4. 考虑可维护性和扩展性:单例类通常不应该做太多事情。如果发现单例类的功能不断增加,可能是时候考虑将其分解为更小的部分。
  5. 释放资源:如果单例持有重要的资源(如文件句柄、数据库连接等),确保在不再需要时释放这些资源。

其三,如果在Android中使用,需要特别注意内存泄漏:如果单例持有一个对Activity、View或其他具有生命周期的对象的长时间引用,可能会导致内存泄漏。当那个Activity应该被垃圾回收时,由于单例的引用,它不会被销毁,从而导致内存泄漏。

6、结尾

Android中使用单例模式的地方是十分多的,例如:Context.getSystemService()、Application 、Retrofit、Room Database、Dagger/Hilt、EventBus、Shared Preferences等等等,都可以自行学习~

代码在这里GitHub

7、感谢

  1. Design-Patterns-In-Kotlin
  2. Kotlin下的5种单例模式
  3. Java-Design-Patterns