單例在Swift中的正確實現方式
盡管在我之前的博文里我就寫過關于管理狀態的那些坑,但是有時候我們就是無法避免它們。其中一類管理狀態的方式我們耳熟能詳 - 單例。但是在Swift中有好幾種不同的方式來實現一個單例。到底哪一個才是正確的方式呢?在這邊博客里,我將和你好好聊聊單例的歷史和在Swift中單 例正確的實現方式。
如果你想直接就看在Swift中如何正確地寫出單例同時看到證明其“正確性”,你可以直接滾動到這篇博文的底部。
讓我們先回憶一下
Swfit源于Objective-C,高于Objective-C。在Objective-C中,我們是這樣寫單例的:
@interface Kraken : NSObject @end @implementation Kraken + (instancetype)sharedInstance { static Kraken *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[Kraken alloc] init]; }); return sharedInstance; } @end
當我們把它寫出來之后,就能清楚的看到一個單例的架構,接下來我們把單例的規則理一理,以便更好的理解它:
單例的道義
關于單例,有三件事是你必須要記住的:
- 單例必須是唯一的,所以它才被稱為單例。在一個應用程序的生命周期里,有且只有一個實例存在。單例的存在給我們提供了一個唯一的全局狀態。比如我們熟悉的NSNotification,UIApplication和NSUserDefaults都是單例。
- 為了保持一個單例的唯一性,單例的構造器必須是私有的。這防止其他對象也能創建出單例類的實例。感謝所有幫我指出這點的人。
- 為了確保單例在應用程序的整個生命周期是唯一的,它就必須是線程安全的。當你一想到并發肯定一陣惡心,簡單來說,如果你寫單例的方式是錯誤的,就 有可能會有兩個線程嘗試在同一時間初始化同一個單例,這樣你就有潛在的風險得到兩個不同的單例。這就意味著我們需要用GCD的dispatch_once 來確保初始化單例的代碼在運行時只執行一次。
成為獨一無二并只在一個地方做初始化并不難。這篇博客接下來要記住的內容就是要單例遵守了更難察覺的dispatch_once的規則。
Swift單例
打從Swift 1.0開始,就有好幾種創建單例的方法。在這里,這里和這里都有詳細的說明。但誰有空點進去看呢?先來個劇透;一共有四種寫法,請讓我一一道來:
最丑陋的寫法
class TheOneAndOnlyKraken { class var sharedInstance: TheOneAndOnlyKraken { struct Static { static var onceToken: dispatch_once_t = 0 static var instance: TheOneAndOnlyKraken? = nil } dispatch_once(&Static.onceToken) { static.instance = TheOneAndOnlyKraken() } return Static.instance! } }
這種寫法其實就是把Objective-C中的寫法照搬過來。我覺得奇丑無比因為Swift是一門更加簡潔且表達力更強的語言。我們要做的比那些搬運工要好,要比他們好。
結構體寫法
class TheOneAndOnlyKraken { class var sharedInstance: TheOneAndOnlyKraken { struct Static { static let instance = TheOneAndOnlyKraken() } return Static.instance } }
這個在Swift 1.0的時候必須得這么寫,因為那個時候,類還不支持全局類變量。而結構體卻支持。正因為在全局變量上的局限性,我們不得不這么寫。這比直接把 Objective-C那一套搬過來要好,但是還不夠。好玩的是,在Swift 1.2發布后的幾個月后,我還是經常能看到這些的寫法,但這個以后再表。
全局變量寫法(又名“一句話寫法”)
private let sharedKraken = TheOneAndOnlyKraken() class TheOneAndOnlyKraken { class var sharedInstance: TheOneAndOnlyKraken { return sharedInstance } }
在Swift 1.2之后,我們擁有了訪問控制修飾詞和可以使用全局類變量。這意味著我們不用整一個全局變量集群,也可以防止命名空間沖突。這才是我心目中Swift應該有的樣子。
這會你可能會問我為什么沒有在我們的結構體和全局變量實現中看不到dispatch_once。但其實Apple指出,這兩個方法同時滿足了我上面提到的dispatch_once條文。下面就截取他們寫的Swift Blog中的一段話來證明他們以將dispatch_once整合進去了:
“全局變量(靜態成員變量和結構體以及枚舉)的延遲構造器在其被第一次訪問時會加載,并以dispatch_once的方式啟動來確保初始化的原子性。這讓你寫代碼時可以用一種很酷的方式來使用dispatch_once:直接用一個全局變量的構造器去做初始化并用private來修飾。“ — Apple’s Swift Blog
就官方文獻來看,Apple就給出這點說明。但這僅意味著對全局變量和結構體/枚舉的靜態成員我們是有證據的!目前來看,100%不會錯的是用一個 全局變量將一個單例的初始化包在一個隱含了dispatch_once的延遲構造器中。但是我們的全局類變量怎么辦呢?!?!?!?
正確的寫法
class TheOneAndOnlyKraken { static let sharedInstance = TheOneAndOnlyKraken() }
為了寫這篇文章,我做了一番研究。事實上,之所以寫這篇文章,是因為今日在Capital One的一次討論,一位PR希望在我們所有的應用中用統一的方式來寫單例。我們其實知道什么才是單例“正確”的書寫方式,但是我們無法自證。如果不旁征博 引來證明我們是正確的簡直就是徒勞。這是我和缺乏信息的互聯網/博文圈之間的較量。每個人都知道如果網上沒寫那就不是真的。這讓我非常難過。
我的搜索達到了互聯網的盡頭(Google到了第10頁)卻啥也沒得到。難道真就發表一行單例的證明嗎?也許有人做了,就是沒找到而已。
所以我覺得我自己把所有的初始化構造器的方法都實現一遍,然后通過斷點來檢查他們。當分析完每個棧幀我所發現的相似點的時候,我終于發現期待已久的東西 - 證據!
直接上圖,對了(還有emoji類哦!):
使用全局單例
使用一行單例
第一張圖顯示了一個全局let實例化。紅框表示的地方就是證據。在實際去實例化Karken單例之前,是先由一個swfit_once調用了一個swift_once_block_invoke。加上Apple說他們通過一個dispatch_once的block去延遲初始化一個全局變量,我們現在可以說這就證明了他們所說的。
帶著這個信息,我又跟蹤了我們既亮眼又簡潔的一行單例。正如第二張圖所以,兩者簡直一樣!這就足以證明我們的一行單例實現是正確的。現在全世界都清凈了。還有,既然這篇文章已經上了互聯網,那么它肯定就是真理!
不要忘了INIT的私有化!
@davedelong,Apple 的架構師,非常含蓄的給我指出,你必須確保你的inits是私有的。只有這樣才能確保你的單例是真正的獨一無二,也能防止其他對象通過訪問控制機制來創建 他們自己的但是是你這個類的單例。因為在Swift中,所有對象的構造器默認都是public,你需要重寫你的init讓其成為私有的。這并不難實現而且 也能確保我們的一行單例的優雅和簡潔:
class TheOneAndOnlyKraken { static let sharedInstance = TheOneAndOnlyKraken() private init() {} // 這就阻止其他對象使用這個類的默認的'()'初始化方法 }
這么做能確保任何類如果嘗試通過()初始化方法來初始化TheOneAndOnlyKraken時,編譯器都會報錯:
你看!這就是完美的,一行實現單例。
結論
呼應一下jtbandes在top rated answer to swift singletons on Stack Overflow上 精彩的評論,我就無法找到關于使用’let’就能確保線程安全的任何文獻。我其實依稀記得在去年的WWDC上有類似的這么一個說法,但你可不能指望讀者或 者googlers在嘗試證明這就是在Swift中寫單例的正確方法的時候就恰巧碰到那說法。不管怎么講,我希望這篇博文能幫助一些人理解一行單例在 Swift中是打開單例的正確方式。
基友們 Happy Coding!
- 原文鏈接 : The Right Way to Write a Singleton
- 原文作者 : Hector Matos
- 譯文出自 : 開發技術前線 www.devtf.cn
- 譯者 : Gottabe