簡(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")
接下來(lái)給person 入住公寓, Apartment記錄person
// 由于john是可選型, 訪問(wèn)的時(shí)候需要解包
john!.apartment = unit4A
unit4A!.tenant = john
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
可以看到j(luò)ohn 強(qiáng)引用Apartment Apartment弱引用john
john = nil
// Prints "John Appleseed is being deinitialized"
同樣
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
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!)
因?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)于weak和unowned引用的示例涵蓋了兩種更常見(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>"
即使閉包內(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>"
paragraph 被釋放
paragraph = nil
// Prints "p is being deinitialized"