Swift 循環(huán)引用

簡(jiǎn)介

Swift 使用 Automatic Reference Counting (ARC) 管理應(yīng)用內(nèi)存的使用,ARC自動(dòng)釋放那些不在使用的對(duì)象,然而在一些場(chǎng)景下ARC需要更多的對(duì)象之間的引用信息來(lái)管理內(nèi)存.

ARC 如何工作

每當(dāng)你創(chuàng)建一個(gè)實(shí)例instance對(duì)象時(shí),ARC分配一塊兒內(nèi)存用來(lái)存儲(chǔ)instance對(duì)象信息包括對(duì)象類型,以及屬性的值.
此外,當(dāng)instance對(duì)象不在使用的時(shí)候,ARC釋放instance對(duì)象所占的內(nèi)存,以便釋放的內(nèi)存可在利用.然而,
instance對(duì)象被ARC釋放后,將不在允許訪問(wèn)該instance對(duì)象的屬性或者方法,如果你嘗試訪問(wèn),結(jié)果就會(huì)使APP crash
為了確保正在使用的instance對(duì)象,不被釋放. ARC追蹤分配給instance對(duì)象的屬性 property 常量 變量 即 引用計(jì)數(shù).只要instance對(duì)象被引用著,就不會(huì)被釋放.

ARC 的作用
下面一個(gè)Person類對(duì)象 有一個(gè)name 常量屬性 一個(gè)初始化方法并賦值給name屬性,一個(gè)析構(gòu)方法

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

定義三個(gè)Person? 類型的變量 reference1 reference2 reference3默認(rèn)值為nil

var reference1: Person?
var reference2: Person?
var reference3: Person?

創(chuàng)建Person類的實(shí)例對(duì)象 reference1 強(qiáng)引用Person instance

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

reference2 reference3 強(qiáng)引用Person instance

reference2 = reference1
reference3 = reference1

通過(guò)賦值nil給 reference1 reference2 使得Person instance引用變?yōu)?,ARC將不會(huì)釋放 Person instance

reference1 = nil
reference2 = nil

當(dāng)最后一個(gè)強(qiáng)引用設(shè)置為nil的時(shí)候,Person instance執(zhí)行了析構(gòu)函數(shù)

reference3 = nil
// Prints "John Appleseed is being deinitialized"

對(duì)象間的循環(huán)引用

在上面的例子??中,ARC能過(guò)追蹤Person instance的引用計(jì)數(shù),進(jìn)行內(nèi)存管理. 然而,我們很容易寫出instance對(duì)象不存在強(qiáng)引用情況的代碼,發(fā)生在兩個(gè)class instances直接彼此強(qiáng)引用.(各位對(duì)方的屬性)稱為引用循環(huán)

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 }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

定義Person Apartment變量 并初始化

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
初始化后 john unit4引用關(guān)系圖

接下來(lái)給person 入住公寓, Apartment記錄person

// 由于john是可選型, 訪問(wèn)的時(shí)候需要解包
john!.apartment = unit4A
unit4A!.tenant = john
Person 入住后 Apartment記錄后.png
john = nil
unit4A = nil

可以看到Person跟Apartment之間的強(qiáng)引用環(huán),因此,當(dāng)你打破john對(duì)Person 跟unit4對(duì)Apartment的強(qiáng)引用時(shí),Person和Apartment之間的閉環(huán)仍然存在,此時(shí)john unit4不會(huì)被ARC回收.(造成內(nèi)存泄漏)

解決

Swift提供了兩種方式 在屬性 類聲明前加 weak或者 unowned ,weak或者 unowned引用允許一個(gè)instance非強(qiáng)引用令一個(gè)instance,來(lái)避免出現(xiàn)強(qiáng)循環(huán).
那什么時(shí)候用weak什么時(shí)候用unowned呢? weak允許使用在生命周期較短的那一方,unowned稍后再講. so 在Person 跟Apartment這個(gè)場(chǎng)景中, Apartment 的生命周期肯定是比Person要長(zhǎng)的.

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?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

一樣的初始化 并彼此關(guān)聯(lián)

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john
weak 修飾后的引用關(guān)系圖.png

可以看到j(luò)ohn 強(qiáng)引用Apartment Apartment弱引用john

john = nil
// Prints "John Appleseed is being deinitialized"
john打破強(qiáng)引用后

同樣

unit4A = nil
// Prints "Apartment 4A is being deinitialized"
unit4打破強(qiáng)引用后

In systems that use garbage collection, weak pointers are sometimes used to implement a simple caching mechanism because objects with no strong references are deallocated only when memory pressure triggers garbage collection. However, with ARC, values are deallocated as soon as their last strong reference is removed, making weak references unsuitable for such a purpose.
在使用垃圾收集的系統(tǒng)中泼各,弱指針有時(shí)用于實(shí)現(xiàn)簡(jiǎn)單的緩存機(jī)制姿现,因?yàn)橹挥性趦?nèi)存壓力觸發(fā)垃圾收集時(shí)才釋放沒(méi)有強(qiáng)引用的對(duì)象。然而脆粥,使用ARC软棺,值在其最后一個(gè)強(qiáng)引用被刪除后立即被釋放红竭,這使得弱引用不適合用于此目的。

unowned
weak一樣喘落,unowned也不會(huì)對(duì)它引用的實(shí)例保持強(qiáng)控制茵宪。但是,與weak不同的是瘦棋,當(dāng)其他實(shí)例具有相同的生命周期期或更長(zhǎng)的生命周期時(shí)稀火,將使用unowned。通過(guò)在屬性或變量聲明前放置unowned關(guān)鍵字赌朋,可以指示一個(gè)unowned引用凰狞。

一個(gè)unowned應(yīng)該總是有一個(gè)值。因此沛慢,ARC從不將unowned引用的值設(shè)置為nil赡若,這意味著unowned引用是使用非可選類型定義的。

只有在確定引用始終引用未釋放的實(shí)例時(shí)团甲,才使用unowned逾冬。如果在釋放實(shí)例之后嘗試訪問(wèn)一個(gè)unowned的值,將會(huì)得到一個(gè)運(yùn)行時(shí)錯(cuò)誤躺苦。

接下來(lái)的例子??中,Customer客戶 CreditCard信用卡 每個(gè)人都有可能有一張信用卡,也有可能沒(méi)有信用卡. 但是一張信用卡必定有一個(gè)對(duì)應(yīng)的客戶. 那么Customer跟CreditCard之間必定存在一個(gè)強(qiáng)引用循環(huán).此時(shí)使用unowned避免循環(huán)引用

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

創(chuàng)建一個(gè)Customer Instance 并設(shè)置CreditCard

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john CreditCard創(chuàng)建后引用關(guān)系

因?yàn)闆](méi)有對(duì)Customer實(shí)例的更強(qiáng)引用身腻,所以john被釋放了。在此之后匹厘,就不再有對(duì)CreditCard實(shí)例的強(qiáng)引用嘀趟,它也被釋放

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

上面的示例展示了如何使用安全的unowned引用。Swift還為需要禁用運(yùn)行時(shí)安全檢查(例如出于性能原因)的情況提供了不安全的unowned引用愈诚。與所有不安全的操作一樣去件,您將負(fù)責(zé)檢查代碼的安全性。通過(guò)編寫unowned(不安全)來(lái)指示一個(gè)不安全的unowned引用扰路。如果您試圖在它引用的實(shí)例被釋放后訪問(wèn)一個(gè)不安全的unowned引用尤溜,那么您的程序?qū)L試訪問(wèn)實(shí)例曾經(jīng)所在的內(nèi)存位置,這是一個(gè)不安全的操作汗唱。

Unowned和隱式展開的可選屬性
上面關(guān)于weakunowned引用的示例涵蓋了兩種更常見(jiàn)的場(chǎng)景宫莱,需要打破強(qiáng)引用循環(huán)。

Person和Apartment的例子顯示了這樣一種情況哩罪,兩個(gè)屬性都被允許為nil授霸,有可能導(dǎo)致強(qiáng)引用循環(huán)巡验。此場(chǎng)景最好使用弱引用來(lái)解決。

Customer和CreditCard示例顯示了一種情況碘耳,其中一個(gè)屬性允許為nil显设,而另一個(gè)屬性不能為nil,這兩種屬性都有可能導(dǎo)致強(qiáng)引用循環(huán)辛辨。此場(chǎng)景最好使用unowned引用來(lái)解決捕捂。

然而,還有第三種情況斗搞,在這種情況下指攒,兩個(gè)屬性都應(yīng)該始終有一個(gè)值,并且一旦初始化完成僻焚,任何一個(gè)屬性都不應(yīng)該為nil允悦。在這個(gè)場(chǎng)景中,將一個(gè)類上的unowned屬性與另一個(gè)類上的隱式展開的可選屬性相結(jié)合是很有用的虑啤。

這使得初始化完成后可以直接訪問(wèn)這兩個(gè)屬性(沒(méi)有可選的展開)隙弛,同時(shí)仍然避免了引用循環(huán)。本節(jié)將向你展示如何建立這樣的關(guān)系狞山。

下面的示例定義了兩個(gè)類全闷,Country和City,每個(gè)類都將另一個(gè)類的實(shí)例存儲(chǔ)為屬性铣墨。在這個(gè)數(shù)據(jù)模型中室埋,每個(gè)國(guó)家必須始終有一個(gè)首都城市办绝,并且每個(gè)城市必須始終屬于一個(gè)國(guó)家伊约。為了表示這一點(diǎn),Country class有一個(gè)capital - City property孕蝉,而City class有一個(gè)Country property:

class Country {
    let name: String
    var capitalCity: City!
    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
    }
}

要設(shè)置這兩個(gè)類之間的相互依賴關(guān)系屡律,City的初始化器接受一個(gè)Country實(shí)例,并將該實(shí)例存儲(chǔ)在其Country屬性中降淮。

City的初始化器從Country的初始化器中調(diào)用超埋。但是,Country的初始化器不能將self傳遞給City初始化器佳鳖,直到一個(gè)新的Country實(shí)例被完全初始化霍殴,如兩階段初始化中所述。

為了滿足這一要求系吩,你可以將Country的capitalCity屬性聲明為一個(gè)隱式展開的可選屬性来庭,(City!)。這意味著capitalCity屬性的默認(rèn)值為nil穿挨,與任何其他可選屬性一樣月弛,但是不需要像隱式展開Optionals中描述的那樣展開它的值就可以訪問(wèn)它肴盏。

因?yàn)閏apitalCity有一個(gè)默認(rèn)的空值,所以只要Country實(shí)例在其初始化器中設(shè)置了name屬性帽衙,就會(huì)認(rèn)為新Country實(shí)例已經(jīng)完全初始化菜皂。這意味著國(guó)家參考和通過(guò)隱式初始化器可以開始自我財(cái)產(chǎn)一旦該國(guó)名稱屬性設(shè)置。因此Country初始化器可以將self作為一個(gè)參數(shù)傳遞給City初始化設(shè)置City的Country厉萝。

這意味著你可以在一個(gè)語(yǔ)句中創(chuàng)建Country和City實(shí)例恍飘,而不需要?jiǎng)?chuàng)建強(qiáng)引用循環(huán),并且可以直接訪問(wèn)capitalCity屬性冀泻,而不需要使用感嘆號(hào)來(lái)打開其可選值:

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

閉包強(qiáng)引用

當(dāng)你將一個(gè)閉包作為對(duì)象的屬性時(shí),同時(shí)閉包內(nèi)又訪問(wèn)了對(duì)象內(nèi)的屬性 或者方法時(shí).這時(shí)候閉包會(huì)捕獲對(duì)象形成引用閉環(huán).

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

例如常侣,可以將asHTML屬性設(shè)置為閉包,如果text屬性為nil弹渔,則該閉包默認(rèn)為某些文本胳施,以防止表示返回空HTML

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"

sHTML屬性被聲明為惰性屬性,因?yàn)橹挥挟?dāng)元素實(shí)際需要作為某個(gè)HTML輸出目標(biāo)的字符串值呈現(xiàn)時(shí)才需要它肢专。asHTML是一個(gè)惰性屬性舞肆,這意味著您可以在缺省閉包中引用self,因?yàn)樵诔跏蓟瓿汕襰elf已知存在之前博杖,惰性屬性不會(huì)被訪問(wèn)椿胯。

HTMLElement類創(chuàng)建和打印一個(gè)新實(shí)例

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
訪問(wèn)asHTML()閉包后引用關(guān)系

即使閉包內(nèi)使用self多次,只強(qiáng)引用HTMLElement對(duì)象一次

當(dāng)你打破paragraph跟HTMLElement對(duì)象的強(qiáng)引用后 paragraph = nil ,會(huì)發(fā)現(xiàn)HTMLElement析構(gòu)方法并沒(méi)有執(zhí)行.(內(nèi)存泄漏)

解決

捕獲列表中的每一項(xiàng)都是weak鍵字或unowned關(guān)鍵字與對(duì)類實(shí)例(如self)的引用或用某個(gè)值初始化的變量的引用的配對(duì)(如delegate = self.delegate!)。這些對(duì)是在一對(duì)方括號(hào)中編寫的剃根,用逗號(hào)分隔

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

如果閉包沒(méi)有指定參數(shù)列表或返回類型哩盲,因?yàn)樗鼈兛梢詮纳舷挛耐茢喑鰜?lái),那么將捕獲列表放在閉包的最開始狈醉,后面跟著in關(guān)鍵字

lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}

Weak and Unowned References
當(dāng)閉包和它捕獲的實(shí)例總是相互引用廉油,并且總是同時(shí)釋放時(shí)。此時(shí)將閉包中的捕獲定義為一個(gè)unowned引用

相反苗傅,當(dāng)捕獲的引用可能在將來(lái)的某個(gè)時(shí)刻變?yōu)閚il時(shí)抒线,將捕獲定義為weak引用。weak引用始終是可選的類型渣慕,當(dāng)它們引用的實(shí)例被釋放時(shí)嘶炭,將自動(dòng)變?yōu)閚il。這使你能夠檢查它們是否存在于閉包中

如果捕獲的引用永遠(yuǎn)不會(huì)變?yōu)閚il逊桦,則應(yīng)該始終將其捕獲為unowned引用眨猎,而不是weak引用。
so,HTMLElement將適合使用unowned引用

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

創(chuàng)建HTMLElement實(shí)例 paragraph

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
unowned 修飾后

paragraph 被釋放

paragraph = nil
// Prints "p is being deinitialized"

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末强经,一起剝皮案震驚了整個(gè)濱河市睡陪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌夕凝,老刑警劉巖宝穗,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件户秤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡逮矛,警方通過(guò)查閱死者的電腦和手機(jī)鸡号,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)须鼎,“玉大人鲸伴,你說(shuō)我怎么就攤上這事〗兀” “怎么了汞窗?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)赡译。 經(jīng)常有香客問(wèn)我仲吏,道長(zhǎng),這世上最難降的妖魔是什么蝌焚? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任裹唆,我火速辦了婚禮,結(jié)果婚禮上只洒,老公的妹妹穿的比我還像新娘许帐。我一直安慰自己,他們只是感情好毕谴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布成畦。 她就那樣靜靜地躺著,像睡著了一般涝开。 火紅的嫁衣襯著肌膚如雪循帐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天忠寻,我揣著相機(jī)與錄音惧浴,去河邊找鬼存和。 笑死奕剃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捐腿。 我是一名探鬼主播纵朋,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼茄袖!你這毒婦竟也來(lái)了操软?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宪祥,失蹤者是張志新(化名)和其女友劉穎聂薪,沒(méi)想到半個(gè)月后家乘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡藏澳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年仁锯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翔悠。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡业崖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蓄愁,到底是詐尸還是另有隱情双炕,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布撮抓,位于F島的核電站妇斤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丹拯。R本人自食惡果不足惜趟济,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咽笼。 院中可真熱鬧顷编,春花似錦、人聲如沸剑刑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)施掏。三九已至钮惠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間七芭,已是汗流浹背素挽。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狸驳,地道東北人预明。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像耙箍,于是被迫代替她去往敵國(guó)和親撰糠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349