原文:Kotlin singletons with argument ——object has its limits
做者:Christophe Beyls
译者:却把清梅嗅html
在Kotlin
中,单例模式被用于替换该编程语言中不存在的static
成员和字段。 你经过简单地声明object
以建立一个单例:java
object SomeSingleton
复制代码
与类不一样,object
不容许有任何构造函数,若是有须要,能够经过使用 init
代码块进行初始化的行为:android
object SomeSingleton {
init {
println("init complete")
}
}
复制代码
这样object
将被实例化,而且在初次执行时,其init
代码块将以线程安全的方式懒惰地执行。 为了这样的效果,Kotlin
对象实际上依赖于Java
的 静态代码块 。上述Kotlin的 object
将被编译为如下等效的Java
代码:git
public final class SomeSingleton {
public static final SomeSingleton INSTANCE;
private SomeSingleton() {
INSTANCE = (SomeSingleton)this;
System.out.println("init complete");
}
static {
new SomeSingleton();
}
}
复制代码
这是在JVM上实现单例的首选方式,由于它能够在线程安全的状况下懒惰地进行初始化,同时没必要依赖复杂的双重检查加锁(double-checked locking)
等加锁算法。 经过在Kotlin中
简单地使用object
进行声明,您能够得到安全有效的单例实现。github
图:无尽的孤独——单例(译者:做者的描述让我想起了一个悲情的角色,Maiev Shadowsong)算法
可是,若是初始化的代码须要一些额外的参数呢?你不能将任何参数传递给它,由于Kotlin
的object
关键字不容许存在任何构造函数。数据库
有些状况下,将参数传递给单例初始化代码块是被推荐的方式。 替代方法要求单例类须要知道某些可以获取该参数的外部组件(component)
,但违反了关注点分离的原则而且使得代码不可被复用。编程
为了缓解这个问题,该外部组件能够是 依赖注入系统。这的确是一个具备可行性的解决方案,但您并不老是但愿使用这种类型的库——而且,在某些状况下您也没法使用它,就像在接下来的Android示例中我将会所提到的。api
在Kotlin中,您必须经过不一样的方式去管理单例的另外一种状况是,单例的具体实现是由外部工具或库(好比Retrofit
,Room
等等)生成的,它们的实例是经过使用Builder
模式或Factory
模式来获取的——在这种状况下,您一般将单例经过interface
或abstract class
进行声明,而不是object
。安全
在Android
平台上,您常常须要将Context
实例做为参数传递给单例组件的初始化代码块中,以便它们能够获取 文件路径,读取系统设置或 开启Service等等,但您还但愿避免对其进行静态引用(即便是Application
的静态引用在技术上是安全的)。 有两种方法能够实现这一目标:
Application.onCreate()
中调用初始化全部组件,此时Application
是可用的——这个简单的解决方案的主要缺点是它是经过阻塞主线程的方式来减慢应用程序启动,并初始化了全部组件,甚至包括那些不会当即使用的组件。另外一个不为人知的问题是,在调用此方法以前,Content Provider
也许已经被实例化了(正如文档中所提到的),所以,若Content Provider
使用全局的相关组件,则您必须保证可以在Application.onCreate()
以前初始化该组件,不然您的申请依然可能会致使应用崩溃。Context
参数。该单例将在第一次调用该函数时使用此参数进行建立和初始化操做。这须要一些同步机制才能保证线程的安全。使用此模式的标准Android
组件的示例是LocalBroadcastManager
:LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
复制代码
咱们能够经过封装逻辑来懒惰地在SingletonHolder
类中建立和初始化带有参数的单例。
为了使该逻辑的线程安全,咱们须要实现一个同步算法,它是最有效的算法,同时也是最难作到的——它就是 双重检查锁定算法(double-checked locking algorithm)
。
open class SingletonHolder<out T, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
复制代码
请注意,为了使算法正常工做,这里须要将@Volatile
注解对instance
成员进行标记。
这可能不是最紧凑或优雅的Kotlin
代码,但它是为双重检查锁定算法生成最行之有效的代码。请信任Kotlin
的做者:实际上,这些代码正是从Kotlin
标准库中的 lazy() 函数的实现中直接借用的,默认状况下它是同步的。它已被修改成容许将参数传递给creator
函数。
有鉴于其相对的复杂性,它不是您想要屡次编写(或者阅读)的那种代码,实际上其目标是,让您每次必须使用参数实现单例时,都可以重用该SingletonHolder
类进行实现。
声明getInstance()
函数的逻辑位置在singleton类的伴随对象内部,这容许经过简单地使用单例类名做为限定符来调用它,就好像Java
中的静态方法同样。Kotlin
的伴随对象提供的一个强大功能是它也可以像任何其余对象同样从基类继承,从而实现与仅静态继承至关的功能。
在这种状况下,咱们但愿使用SingletonHolder
做为单例类的伴随对象的基类,以便在单例类上重用并自动公开其getInstance()
函数。
对于SingletonHolder
类构造方法中的creator
参数,它是一个函数类型,您能够声明为一个内联(inline)
的lambda,但更经常使用的状况是 做为一个函数引用的依赖交给构造器,最终其代码以下所示:
class Manager private constructor(context: Context) {
init {
// Init using context argument
}
companion object : SingletonHolder<Manager, Context>(::Manager)
}
复制代码
如今可使用如下语法调用单例,而且它的初始化将是lazy
而且线程安全的:
Manager.getInstance(context).doStuff()
复制代码
当三方库生成单例实现而且Builder
须要参数时,您也可使用这种方式,如下是使用Room
数据库的示例:
@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object : SingletonHolder<UsersDatabase, Context>({
Room.databaseBuilder(it.applicationContext,
UsersDatabase::class.java, "Sample.db")
.build()
})
}
复制代码
注意:当Builder
不须要参数时,您只需使用lazy
的属性委托:
interface GitHubService {
companion object {
val instance: GitHubService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
retrofit.create(GitHubService::class.java)
}
}
}
复制代码
我但愿这些代码可以给您带来一些启发。若是您有建议或疑问,请不要犹豫,在评论部分开始讨论,感谢您的阅读!
--------------------------广告分割线------------------------------
Hello,我是却把清梅嗅,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人博客或者Github。
若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?