原文: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。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?