Swift:weak與unowned的奧秘

Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

However, in a few cases ARC requires more information about the relationships between parts of your code in order to manage memory for you.

以上是 Swift 官方教程中,對 Swift 內存管理的解釋。通常情況的部分很好理解,Swift 中 ARC 負責絕大部分的內存管理,ARC部分可以參考我的另一篇博客:iOS內存管理初探 – 引用計數、AutoRelease與ARC;但少數情況下,我們需要向ARC提供對象之間的關系來使其正確管理內存。那么少數情況是什么意思呢?這得從Swift中的循環強引用講起。

Swift 中循環強引用的情景

  • 類實例之間的循環強引用:類實例之間相互強引用了對方
類實例之間的循環引用.png

如圖所示的情況中,john指向的對象強引用了unit4A指向的對象,而unit4A指向的對象又強引用了john指向的對象。

  • 閉包引起的循環強引用
閉包循環引用.png

paragraph實例有一個成員asHTML強引用了一個閉包,而這個閉包中又捕獲了self,意味著對self的強引用。

在ARC下,引用循環的情況是編譯器無法自動解決的,這就是上文提到的少數情況。weak 和 unowned 的存才就是為了給編譯器提供更多的信息,來打破循環引用。

利用 weak 和 unowned 殺死循環引用

weak

含義:weak 即弱引用,當把一個實例聲明為弱引用時,此實例不會持有這個對象,即不會使對象的引用計數加1。當對象被廢棄,其所有的弱引用會被置為 nil。

適用場景

  • 類實例之間的循環強引用:實例之間形成引用循環,且無法確定對象之間生命周期的依賴關系,即無法確定弱引用在某一時刻是否為空時,將可能為空的實例聲明為弱引用。由于弱引用可能為 nil,應當聲明為可選值。如:
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?  //公寓的房客可能為空所以聲明為weak
    deinit { print("Apartment \(unit) is being deinitialized") }
}

由于 tenant 是弱引用,當 tenant 引用的對象被銷毀(如賦值 nil),tenant 被置為空,并且釋放對 apartment的強引用,此時 apartment 所指對象就可以正常釋放了。

  • 閉包引起的循環強引用:在被捕獲的引用可能會變為nil時,在捕獲列表中,將閉包內的捕獲定義為弱引用。如:
// someClosure 是類成員變量
lazy var someClosure: (Int, String) -> String = {
    //self.delegate 可能在被捕獲后變為 nil,所以定義為弱引用,unowned 解釋見下文
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // 這里是閉包的函數體
}

由于 self.delegate 指向的是外部對象,生命周期與self無關,所以可能在被捕獲后變為nil。(delegate 一般都聲明為weak以避免循環引用)

unowned

含義:無主引用,與弱引用一樣,當把一個實例聲明為無主引用時,此實例不會持有這個對象,即不會使對象的引用計數加1。但與弱引用不同的是,當對象被廢棄,其無主引用并不會被置為 nil。

適用場景

  • 類實例之間的循環強引用:實例之間形成引用循環,且可以確定某一實例之外的其他實例有相同或者更長的生命周期時,將此實例聲明為無主引用。無主引用總被期望擁有值,當你訪問對象被銷毀的無主引用時,會觸發運行時錯誤。
class Country {
    let name: String
    var capitalCity: City!  //由于 capitalCity 的生命周期等于 country 的生命周期,可以隱式解析可選屬性
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

雖然在這個例子中,capitalCity 與 country 的生命周期相同,理論上講將其中任何一個聲明為無主引用都可以打破引用循環,但 capitalCity 與 country 之間有從屬關系,所以傾向于將“大”的一方,即 country 聲明為無主引用。

  • 閉包引起的循環強引用:在閉包和捕獲的實例總是互相引用并且總是同時銷毀時,將閉包內的捕獲定義為無主引用。
// someClosure 是類成員變量
lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // 這里是閉包的函數體
}

self 與屬于 self 的成員變量 someClosure 生命周期相同,同時銷毀,所以聲明為無主引用。

unowned(safe) 與 unowned(unsafe)

Automatic Reference Counting

Swift also provides unsafe unowned references for cases where you need to disable runtime safety checks—for example, for performance reasons. As with all unsafe operations, you take on the responsiblity for checking that code for safety.

You indicate an unsafe unowned reference by writing unowned(unsafe). If you try to access an unsafe unowned reference after the instance that it refers to is deallocated, your program will try to access the memory location where the instance used to be, which is an unsafe operation.

在Swift中,寫下 unowned 相當于 unowned(safe)。但在官方文檔中提到, Swift 還提供了另一種不安全的無主引用 unowned(unsafe) 來禁用運行時的安全檢查。運行時的安全檢查就是使 unowned(safe) 安全的原因。

unowned(safe):當訪問 unowned(safe) 類型的無主引用時,運行時會進行安全檢查,如果對象已經廢棄,將拋出異常并終止程序。

unowned(unsafe) :unowned(unsafe) 的作用效果實際上相當于 Objective-C 屬性標示符中的 assign/unsafeunretained。訪問 unowned(unsafe) 類型的無主引用時,運行時的安全檢查被禁用,這時會有三種情況:

  • 廢棄對象內存還未被覆蓋:程序正常運行
  • 廢棄對象內存被部分覆蓋:奇怪的 crash,不確定的結果
  • 廢棄對象內存正好填入新的對象:由于向新的對象發送了調用舊對象方法的消息,會出現 unrecognized selector exceptions

總結

weak unowned
能殺死的強引用循環 實例之間/ 閉包引起 實例之間/ 閉包引起
實例是否能為nil 都允許為nil 其中一個允許為nil,另一個不允許為nil
實例間的生命周期關系 無法確定 一個小于等于另一個

參考文章

The Swift Programming Language (Swift 3.1) : Automatic Reference Counting
What is the difference in Swift between 'unowned(safe)' and 'unowned(unsafe)'?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容