在Kotlin中如何设计带参数的单例模式

深入学习Kotlin基础知识
2021-05-17 14:29 · 阅读时长8分钟
小课
一、无参的单例

单例模式是开发中常用的设计模式,在Kotlin中,创建单例非常简单,只需要使用object来声明即可,比如

object SomeSingleton

普通的类class可以有构造方法,而object不能有构造方法,但是init代码块还是可以使用,当object被实例化第一次调用时,init代码块会以线程安全的方式执行,比如

object SomeSingleton {
    init {
        println("init complete")
    }
}

我们知道kotlin最后会编译成class字节码,它最终的实现其实等同下面的Java代码

public final class SomeSingleton {
   public static final SomeSingleton INSTANCE;
   private SomeSingleton() {
      INSTANCE = (SomeSingleton)this;
      System.out.println("init complete");
   }
   static {
      new SomeSingleton();
   }
}

其实这也是Java中实现单例的一种方式,并且是比较推荐的方式,因为它不仅满足懒加载,而且还保证了线程安全,并且没有一些复杂的锁操作。

另外还可以通过by lazy的方式去实现单例,比如在使用retrofit时,为service创建单例。

interface ApiService {
    
    companion object {
      
        val instance: ApiService by lazy {
            Retrofit.Builder()....build().create(ApiService::class.java)
        }
    }
}
二、带参的单例

因为object申明的对象是不能有构造方法,那么如果在单例初始化时需要参数,我们该怎么设计呢?比如说在Android开发中,我们经常需要传入一个Context的实例,一般我们有以下三种方式来实现:

一、在Application.onCreate()的时候,将applicationContext保存在一个全局静态变量中,然后在object的init代码块中拿到这个context实例进行初始化,这样的好处是:

  • 能实现懒加载。
  • 后续获取单例不用再传参。

但是缺点是需要将Context保存到静态变量中。参考代码实现如下:

1object GlobalConfig {
2    lateinit var context: Context
3}
4
5class App : Application() {
6    override fun onCreate() {
7        super.onCreate()
8        GlobalConfig.context = applicationContext
9    }
10}
11
12object Singleton {
13    init {
14        initialize(GlobalConfig.context)
15    }
16    private fun initialize(context: Context) {
17        //...
18    }
19}

在Application.onCreate()的时候,拿到context实例,并初始化单例,这样的好处是:

  • 不用保存context实例到静态变量中。
  • 后续获取单例不用再传参。

但是缺点是在Application.onCreate()时就实例化单例了,无法实现懒加载。参考代码实现如下:

1class App : Application() {
2    override fun onCreate() {
3        super.onCreate()
4        Singleton.initialize(applicationContext)
5    }
6}
7
8object Singleton {
9    fun initialize(context: Context) {
10        //...
11    }
12}

在获取单例时传递Context参数,然后通过双重锁来实现创建单例,这样的好处是:

  • 不用保存context实例到静态变量中。
  • 实现了懒加载。

但是缺点是后续获取单例需要传参,这种方式是比较推荐的,在Android官方代码中也有使用到,比如WorkManagerWallpaperManager,参考代码实现如下:

1class Singleton private constructor(context: Context){
2    companion object {
3        @Volatile private var instance: Singleton? = null
4
5        fun getInstance(context: Context): Singleton {
6            val i = instance
7            if (!= null) {
8                return i
9            }
10            return synchronized(this) {
11                val i2 = instance
12                if (i2 != null) {
13                    i2
14                } else {
15                    val created = Singleton(context)
16                    instance = created
17                    created
18                }
19            }
20        }
21    }
22
23    init {
24        initialize(context)
25    }
26
27    private fun initialize(context: Context) {
28        //...
29    }
30}

对于第三种方式,我们可以通过封装来复用代码,首先封装一个通用的类SingletonHolder来实现双重锁创建单例。

1open class SingletonHolder<out T: Any, in A>(creator: (A) -> T) {
2    private var creator: ((A) -> T)? = creator
3    @Volatile private var instance: T? = null
4
5    fun getInstance(arg: A): T {
6        val i = instance
7        if (!= null) {
8            return i
9        }
10        return synchronized(this) {
11            val i2 = instance
12            if (i2 != null) {
13                i2
14            } else {
15                val created = creator!!(arg)
16                instance = created
17                creator = null
18                created
19            }
20        }
21    }
22}

然后在需要的地方调用,这样代码就清爽多了。

class Singleton private constructor(context: Context) {
    companion object : SingletonHolder<Singleton, Context>(::Singleton)
    init {
        initialize(context)
    }
    private fun initialize(context: Context) {
        //...
    }
}
1@Database(entities = arrayOf(User::class), version = 1)
2abstract class UsersDatabase : RoomDatabase() {
3
4    abstract fun userDao(): UserDao
5
6    companion object : SingletonHolder<UsersDatabase, Context>({
7        Room.databaseBuilder(it.applicationContext,
8                UsersDatabase::class.java, "Sample.db")
9                .build()
10    })
11}
singletonkotlin单例