聊聊Kotlin單例,從object單例,到帶參數單例,論如何優雅的封裝!

一. 序

單例模式是我們在日常編程中,比較常用的設計模式。一個好的單例,必然需要滿足唯一性和線程安全性。而 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 表現的特點如下:

  1. 類用 final 標記,標識不可變性。

  2. 內部聲明一個 static final 的當前類的對象 INSATNCE。

  3. 在靜態代碼塊中,進行 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 下,實現單例模式的一些代碼技巧,希望對大家有所幫助。最後再簡單總結一下。

  1. 無參單例模式,直接使用 Kotlin 的 object 即可,它是依賴類的初始化鎖來保證線程安全。

  2. 帶參單例模式,可以使用雙重檢查鎖 + @Volatile 來實現,如果嫌麻煩還可以封裝成 SingletonHolder。

  3. lazy() 委託確實可以實現延遲加載,但是在單例模式的場景下,不如直接用 object 方便。

本文對你有幫助嗎?留言、轉發、點好看是最大的支持,謝謝!

reference:

https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e


聯機圓桌」????推薦我的知識星球,一年 50 個優質問題,上桌聯機學習。

公衆號後臺回覆成長『成長』,將會得到我準備的學習資料,也能回覆『加羣』,一起學習進步;你還能回覆『提問』,向我發起提問。

發佈了327 篇原創文章 · 獲贊 8 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章