一. 序
單例模式是我們在日常編程中,比較常用的設計模式。一個好的單例,必然需要滿足唯一性和線程安全性。而 Java 中,關於單例的文章講解已經很完善了,單例模式已經成爲一種編程範式。
在谷歌強推 Kotlin 的今天,不少人使用 Kotlin 時,還帶着 Java 的編程思維,並沒有有效的利用 Kotlin 的一些特性。如果還用 Java 的編程思想來寫 Kotlin 的單例,會有種四不像的感覺。
在 Kotlin 裏,想要實現單例模式,只需要將類增加 object
關鍵字即可,這就是一個線程安全的單例模式,很方便。
但是這存在一個問題,object class
無法實現構造方法,也就是我們無法在初始化的時候,從外部傳遞一些參數來讓這個單例類初始化。
本文就來聊聊 Kotlin 下的單例模式的實現,以及如何優雅的構造一個帶參數的單例模式。
二. Kotlin 的單例
2.1 object class 的單例
雖然無法在構造的時候,從外部傳遞參數,但是 object
關鍵字依然是 Kotlin 下,最常用的構造單例方法,我們先來了解它的特性。
object 關鍵字使用起來非常簡單,只需要直接作用在 class 上就好。
object SomeSingleton{
fun sayHi(){}
}
這就是在 Kotlin 下,最簡單的單例模式,如果想要有一些初始化的動作,可以放在 init 塊中。
object SomeSingleton{
init{
// init
}
fun sayHi(){}
}
使用方法也非常簡單,需要注意的是,在 Kotlin 中調用和 Java 調用存在一些差異。
// Kotlin Language
SomeSingleton.sayHi()
// Java Language
SomeSingleton.INSTANCE.sayHi()
我們知道,Kotlin 和 Java 是可以無縫互通的,而 Kotlin 最終編譯的字節碼,其實也是可以轉成類 Java 的代碼。
那我們繼續看看 Kotlin 的 object 關鍵字後,在 Java 中的表現到底如何。通過這種轉碼的分析,可以便於我們理解 Kotlin 的特性。
藉助 AS 的 Tools → Kotlin → Show Kotlin Bytecode,就可以查看 Kotlin 文件的字節碼,再點擊 Decompile 按鈕,就可以將字節碼轉成 Java 代碼。
有對比就清晰了,Kotlin 的 object 關鍵字,在 Java 表現的特點如下:
類用 final 標記,標識不可變性。
內部聲明一個 static final 的當前類的對象 INSATNCE。
在靜態代碼塊中,進行 INSTANCE 對象的初始化。
可以看到,在 Kotlin 的 object 中,是使用類的初始化鎖來保證線程安全的。
那什麼是類的初始化鎖?
簡單來說, JVM 在類的初始化階段(即在 Class 被加載後,且被線程使用之前),會執行類的初始化,在初始化期間,JVM 會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化,避免多線程調用時,引發線程安全的問題。
上圖很清晰的表明了類的初始化鎖的工作流程。
而 Kotlin 中的 object 關鍵字,就是利用類的初始化鎖來保證線程安全的,在我們不需要爲單例的初始化傳遞外部參數的場景下,可以放心使用。
那可能有人擔心另一個問題,類加載的時候就初始化構造單例對象,是不是對資源的利用不友好?
這一點問題不大,虛擬機在運行程序的時候,並不是在啓動時就將所有的類,都加載進來並初始化完成,而是一種按需加載的策略,在真正使用它的時候,纔會初始化。
例如:new Class
、調用靜態方法、反射、調用 Class.forName()
方法等。這一點可以通過本文介紹的單例實現,在 init 塊中輸出 Log,看看 Log 何時輸出來驗證,相關資料很多,就不多說了。
也就是說,通常只有在你真實使用這個類時,它纔會真的被虛擬機初始化。當然,不同虛擬機的實現方式不同,這並不是強制的,但是大多數爲了性能都會準守此規則。
2.2 傳參數的單例
無參單例可以用 object 關鍵字,但如果想通過一些外部參數初始化單例呢?Kotlin 的 object 是不能有任何構造函數的,所以也無法傳遞任何參數。
帶參單例在 Android 中也是有一些使用場景的,例如 Android 中的 LocalBroadcastManager,就是一個帶參的單例模式。
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
那換個思路想想,在 Java 中,帶參數的單例如何實現?通常都會用雙重檢查鎖(Double Checked Locking) + volatile 關鍵字來解決。
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton sInstance;
private DoubleCheckSingleton(Context ctx) {
// init
}
public static DoubleCheckSingleton getInstance(Context ctx) {
if (sInstance == null) {
synchronized (DoubleCheckSingleton.class) {
if (sInstance == null) {
sInstance = new DoubleCheckSingleton(ctx);
}
}
}
return sInstance;
}
}
加上 volatile 是爲了可見性和禁止重排序,這樣就可以保證把參數傳遞進去的同時,確保線程安全。
不過在 Kotlin 中是沒有 volatile 關鍵字的,取而代之的是 @Volatile
註解,同時需要配合 Kotlin 的伴生對象進行單例模式的構建。
伴生對象可以簡單的使用類名作爲限定符來調用其方法,類似 Java 中的靜態方法。
final class SomeSingleton(context: Context) {
private val mContext: Context = context
companion object {
@Volatile
private var instance: SomeSingleton? = null
fun getInstance(context: Context): SomeSingleton {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = SomeSingleton(context)
instance = created
created
}
}
}
}
}
這段代碼是直接借鑑的 Kotlin 的 lazy()
,lazy 在默認情況下的實現是 SynchronizedLazyImpl,從類名上就能看出來,它使用 synchroinzed 來保證線程安全。
用這樣的方式,就可以實現一個可以傳參數去構造的單例模式。
2.3 封裝一個帶參單例
支持傳參的單例,我們實現了。但爲了實現這個單例,寫了 20+ 行代碼。每次寫單例都要把這一堆代碼複製一遍,還挺麻煩,爲了使用方便,還可以將其再封裝一下。
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
}
}
}
}
用一個支持繼承的 open class
加上泛型就可以簡單的將其進行封裝,此封裝方式支持一個參數的構造方法,有需要可以繼續擴展或者封裝。
class SomeSingleton private constructor(context: Context) {
init {
// Init using context argument
context.getString(R.string.app_name)
}
companion object : SingletonHolder<SomeSingleton, Context>(::SomeSingleton)
}
封裝成 SingletonHolder 類之後,再想使用單例,關鍵代碼一行就搞定了。
2.4 使用 lazy
前面在介紹帶參單例的時候,也提到了lazy()
,它是 Kotlin 的一種標準委託,可以接受一個 lambda 並返回一個實例的函數。
如果我們想要延遲初始化,可以使用 lazy()
這個代理來實現,它會在第一次調用 get()
方法時,執行 lazy()
的 lambda 表達式並記錄結果,之後再調用 get()
就只會返回之前記錄的結果,非常適合延遲初始化的場景。
class SomeSingleton{
companion object {
val instance: SomeSingleton by lazy { SomeSingleton() }
}
}
lazy()
默認情況下,內部就是依賴同步鎖(synchronized)來實現的,所以它也是線程安全的。
但是正如我前面提到的,類本身也是按需加載的,調用它的下一步肯定是也需要使用它,所以只要我們正確的使用單例模式,其實沒必要使用 lazy()
,這裏僅做一個介紹,大家知道一下就好了。
三. 小結時刻
本文介紹了在 Kotlin 下,實現單例模式的一些代碼技巧,希望對大家有所幫助。最後再簡單總結一下。
無參單例模式,直接使用 Kotlin 的
object
即可,它是依賴類的初始化鎖來保證線程安全。帶參單例模式,可以使用雙重檢查鎖 +
@Volatile
來實現,如果嫌麻煩還可以封裝成 SingletonHolder。lazy()
委託確實可以實現延遲加載,但是在單例模式的場景下,不如直接用object
方便。
本文對你有幫助嗎?留言、轉發、點好看是最大的支持,謝謝!
reference:
https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e
「聯機圓桌」????推薦我的知識星球,一年 50 個優質問題,上桌聯機學習。
公衆號後臺回覆成長『成長』,將會得到我準備的學習資料,也能回覆『加羣』,一起學習進步;你還能回覆『提問』,向我發起提問。