一进苍、內(nèi)存分配
??值類型,比如說枚舉和結(jié)構(gòu)體鸭叙,它們的內(nèi)存分配和管理都十分簡單觉啊。當你新建一個值類型實例時,系統(tǒng)會自動為實例分配大小合適的內(nèi)存沈贝。任何傳遞實例的操作杠人,比如說作為參數(shù)傳遞給函數(shù),以及存儲到屬性的操作,它們都會創(chuàng)建實例的副本嗡善。當實例不再存在時辑莫,Swift會回收內(nèi)存。因此罩引,我們不需要做任何事情來管理值類型的內(nèi)存各吨。
??在Swift中,內(nèi)存管理這個議題蜒程,通常都是和引用類型绅你,尤其是類相關(guān)的伺帘。跟值類型一樣昭躺,當我們新建類實例時,系統(tǒng)會為實例分配內(nèi)存空間伪嫁。但是领炫,和值類型所不同的是,當我們把類實例作為參數(shù)傳遞給函數(shù)张咳,或者將其存儲到屬性中時帝洪,不再是復(fù)制實例本身,而是對同一塊內(nèi)存創(chuàng)建新的引用脚猾。對于同一塊內(nèi)存擁有多個引用的情況葱峡,這意味著,只要其中任何一個引用修改了類的實例龙助,那么所有的引用都將能看到這個變化的結(jié)果砰奕。
??和C語言不同,Swift并不需要我們手動的管理內(nèi)存提鸟,系統(tǒng)會自動為每個類實例維護一個引用計數(shù)(Reference Count
)军援。只要引用計數(shù)大于0,實例就會一直存在称勋;一旦引用計數(shù)變?yōu)?胸哥,實例就會被銷毀,而它所占用的內(nèi)存就會被回收赡鲜,此時deinit
方法就會被調(diào)用空厌。因此喊积,我們可以通過實現(xiàn)deinit方法來追蹤實例是否被銷毀铲咨。
二、循環(huán)引用
??在正式演示循環(huán)引用之前郎嫁,我們先通過一個簡單的例子來觀察一下類實例從創(chuàng)建到最后被銷毀的全過程:
class Person: CustomStringConvertible {
let name: String
// 遵守CustomStringConvertible協(xié)議捡硅,實現(xiàn)
// description計算屬性哮内,自定義打印格式
var description: String {
return "\(name)" // "Person(\(name))"
}
// 構(gòu)造函數(shù)
init(name: String) {
// 初始化私有屬性
self.name = name
}
// 當引用計數(shù)為0時,這個方法會被調(diào)用
deinit {
print("\(self)被銷毀了")
}
}
// 創(chuàng)建一個Person實例,并且對其進行初始化
// 這里需要將實例變量james聲明為可選類型北发,
// 這樣后面就可以給它賦值nil纹因,從而方便調(diào)用
// deinit方法
var james: Person? = Person(name: "James")
print("創(chuàng)建了一個Person類實例\(james!)")
// 默認情況下,所有的引用都是強引用琳拨,這意味著當我們創(chuàng)建
// Person實例james瞭恰,并且給它賦值James時,引用計數(shù)是
// 加1的狱庇。當我們再次給james賦值為nil時惊畏,引用計數(shù)是減1
// 的,這樣一來密任,deinit方法就會被調(diào)用颜启,我們就能看到打印
james = nil
??程序運行之后,我們首先會看到Person實例james被創(chuàng)建浪讳,并且當我們將其重置為nil時缰盏,它就會被銷毀(deinit
方法被調(diào)用):
??接下來,我們要修改程序淹遵。假設(shè)James是一位資深的愛寵人士口猜,它最近買了一條寵物狗,名字叫做旺財透揣。我們先創(chuàng)建一個Dog類济炎,然后再對上面的代碼進行修改:
// Dog.swift
class Dog: CustomStringConvertible {
let name: String
var owner: Person?
// 自定義輸出格式
var description: String {
if let dogOwner = owner {
return "\(name)的主人是\(dogOwner)."
} else {
return "\(name)是一條流浪犬。"
}
}
// 構(gòu)造函數(shù)
init(name: String) {
// 初始化私有屬性
self.name = name
}
// 實例被銷毀時調(diào)用
deinit {
print("\(self)被銷毀了")
}
}
// Person.swift
class Person: CustomStringConvertible {
let name: String
var dogs = [Dog]()
// 遵守CustomStringConvertible協(xié)議辐真,實現(xiàn)
// description計算屬性须尚,自定義打印格式
var description: String {
return "\(name)" // "Person(\(name))"
}
// 構(gòu)造函數(shù)
init(name: String) {
// 初始化私有屬性
self.name = name
}
// 當引用計數(shù)為0時,這個方法會被調(diào)用
deinit {
print("\(self)被銷毀了")
}
// 買了寵物狗
func buyDogs(_ dog: Dog) {
dog.owner = self
dogs.append(dog)
}
}
// main.swift
// 創(chuàng)建一個Person實例
var james: Person? = Person(name: "James")
print("創(chuàng)建了一個Person類實例\(james!)")
// 創(chuàng)建一個Dog實例并且初始化
var wangcai: Dog? = Dog(name: "Wangcai")
// james買了寵物狗wangcai
james?.buyDogs(wangcai!)
// 重新賦值
james = nil
wangcai = nil
??運行上面的程序拆祈,你會發(fā)現(xiàn)恨闪,除了main.swift中的print
語句被打印了之外,Person.swift和Dog.swift中的deinit
方法都沒有被調(diào)用:
??這也就是說放坏,雖然我們最后給實例變量james
和wangcai
賦值為nil
咙咽,但是它們最后都沒有被銷毀。之所以沒有被銷毀淤年,是因為此時james
和wangcai
的引用計數(shù)都不為0钧敞。
??為什么會出現(xiàn)上面這種情況?在前面的注釋中我們說過麸粮,默認情況下溉苛,所有的引用都是強引用,而我們恰好就創(chuàng)建了兩個強引用弄诲,以至于james
強引用wangcai
愚战,而wangcai
又強引用james
娇唯,從而導(dǎo)致指向這兩個實例的變量沒有了,但是他們的內(nèi)存卻不會被回收寂玲。
??循環(huán)引用的一個很嚴重的后果就是內(nèi)存泄漏塔插,也就是當程序已經(jīng)不再需要這些內(nèi)存的時候,它并沒有將其交還給操作系統(tǒng)拓哟。當然想许,如果整個應(yīng)用程序都停止了,這個應(yīng)用所有的內(nèi)存断序,包括泄漏的內(nèi)存都會被操作系統(tǒng)給回收流纹。只不過,在整個應(yīng)用運行期間违诗,過多的內(nèi)存泄漏會導(dǎo)致程序占用內(nèi)存過大漱凝,有可能會被操作系統(tǒng)殺掉的。所以较雕,對于內(nèi)存本來就相對有限的iOS來說碉哑,應(yīng)用程序的內(nèi)存管理是一個值得重視的問題挚币。
??我們已經(jīng)知道了上面實例沒有被銷毀的原因亮蒋,以及實例所占用的內(nèi)存沒有被及時回收的后果,接下來就該知道怎么去避免這種事情的發(fā)生了妆毕。
三慎玖、循環(huán)引用問題的解決
??解決循環(huán)引用最主要的一個手段,就是將兩個相互強引用關(guān)系中的一個變?yōu)槿跻玫颜场wift中提供了一個關(guān)鍵字weak
來處理這種問題趁怔。我們修改Dog.swift中的代碼,用關(guān)鍵字weak
來修飾屬性owner
:
class Dog: CustomStringConvertible {
let name: String
weak var owner: Person?
// 自定義輸出格式
var description: String {
if let dogOwner = owner {
return "\(name)的主人是\(dogOwner)."
} else {
return "\(name)是一條流浪犬薪前。"
}
}
// 構(gòu)造函數(shù)
init(name: String) {
// 初始化私有屬性
self.name = name
}
// 實例被銷毀時調(diào)用
deinit {
print("\(self)被銷毀了")
}
}
??值得注意的是润努,弱引用的使用是有條件的:(1)、弱引用修飾的屬性必須用關(guān)鍵字var
來聲明示括,不能使用let
铺浇;(2)、弱引用修飾的屬性必須聲明為可選類型垛膝。修改完成之后鳍侣,再來運行程序,就能看到我們想要的結(jié)果了:
四吼拥、閉包中的循環(huán)引用
??為了演示閉包中的循環(huán)引用問題倚聚,我們先來新建一個Accountant類,用來記錄Person實例新增的資產(chǎn)凿可,并且根據(jù)實際需求修改Person.swift中的代碼:
// Accountant.swift
class Accountant {
// 使用類型別名來定義一個閉包
typealias NetWorthChanged = (Double) -> ()
var netWorthChangedHandler: NetWorthChanged? = nil
// 凈資產(chǎn)
var netWorth: Double = 0 {
// 監(jiān)聽netWorthChangedHandler的變化
didSet {
netWorthChangedHandler?(netWorth)
}
}
// 增加了新的資產(chǎn)
func gained(_ dog: Dog) {
netWorth += dog.price
}
}
// Person.swift
class Person: CustomStringConvertible {
let name: String
let accountant = Accountant() // 注意惑折,這個是強引用
var dogs = [Dog]()
// 遵守CustomStringConvertible協(xié)議,實現(xiàn)
// description計算屬性,自定義打印格式
var description: String {
return "\(name)" // "Person(\(name))"
}
// 構(gòu)造函數(shù)
init(name: String) {
// 初始化私有屬性
self.name = name
// 給accountant的netWorthChangedHandler賦值
accountant.netWorthChangedHandler = { netWorth in
self.netWorthDidChange(to: netWorth)
return
}
}
// 當引用計數(shù)為0時惨驶,這個方法會被調(diào)用
deinit {
print("\(self)被銷毀了")
}
// 買了寵物狗
func buyDogs(_ dog: Dog) {
dog.owner = self
dogs.append(dog)
// 如果有新增的資產(chǎn)矗积,需要進行記錄
accountant.gained(dog)
}
// 記錄凈資產(chǎn)的變化
func netWorthDidChange(to netWorth: Double) {
print("\(self)又買了一條狗,它現(xiàn)在新增資產(chǎn)的價值是\(netWorth)元敞咧。")
}
}
??在前面休建,我們已經(jīng)解決了實例變量james
和wangcai
之間相互循環(huán)引用的問題测砂。但是茵烈,此時如果運行程序砌些,你又會發(fā)現(xiàn)存璃,實例變量james
和wangcai
又沒有被釋放:
??很顯然粘招,程序中肯定又產(chǎn)生了循環(huán)引用偎球。這是為什么呢衰絮?要弄清這個問題猫牡,我們必須先回顧一下閉包的基礎(chǔ)知識乃戈。首先亩进,閉包是一種特殊的函數(shù)归薛,它可以捕獲和存儲其所在上下文環(huán)境中的變量和常量,即使定義這些變量和常量的原作用域已經(jīng)不存在了逛球,它仍然可以在其函數(shù)體內(nèi)部引用和修改這些值颤绕。其次奥务,閉包是引用類型氯葬,這意味著當你把閉包賦值給變量或者常量時帚称,實際上是讓這個變量或者常量指向這個閉包闯睹,也就是說我們并沒有為這個閉包創(chuàng)建新的副本瞻坝。最后,在閉包中涉及當前類的屬性衙荐,或者調(diào)用當前類的函數(shù)時砌函,必須明確使用self.
讹俊。知道這些東西以后仍劈,再回過頭去看之前的代碼贩疙,很容易就明白為什么我們的項目中存在循環(huán)引用了这溅。
??首先悲靴,Person類中有一個Accountant類型的屬性accountant庇楞。因此吕晌,Person類對Accountant類有一個強引用睛驳;其次,默認情況下爪瓜,閉包對它里面捕獲的變量或者常量有一個強引用蝶缀。而我們在Person類中對Accountant類的屬性netWorthChangedHandler
進行賦值時翁都,是通過self.
進行的,而此時self
恰恰是指代Person類谅猾。因此柄慰,Accountant類又對Person類有一個強引用。那么税娜,如何打破這種循環(huán)引用的關(guān)系呢?
// 在閉包中調(diào)用當前類的方法時概行,如果沒有使用
self.
會報如下錯誤:
Call to method '方法名' in closure requires explicit 'self.' to make capture semantics explicit// 在閉包中使用當前類的屬性時,如果沒有使用
self.
會報如下錯誤:
Reference to property '屬性名' in closure requires explicit 'self.' to make capture semantics explicit
??一個比較好的辦法是谤绳,改變閉包捕獲的語義消略,使捕獲變?yōu)槿跻谩榇讼猓覀冃枰褂?strong>捕獲列表(Capture List)
肿男。捕獲列表的語法是舶沛,在閉包參數(shù)列表的前面嘹承,加上帶方括號的變量列表,通過這種方式來告訴Accountant類使用弱引用來捕獲self(Person):
// 構(gòu)造函數(shù)
init(name: String) {
// 初始化私有屬性
self.name = name
// 給Accountant類的屬性netWorthChangedHandler賦值
accountant.netWorthChangedHandler = { [weak self] netWorth in
self?.netWorthDidChange(to: netWorth)
return
}
}
??需要注意的是如庭,此時self
是弱引用坪它,而所有的弱引用實例,都必須是可選類型哟楷,因此需要將原先的self.
修改為self?.
瘤载。再次運行程序,我們又可以看到Person的實例james和Dog的實例wangcai被正常釋放了:
五卖擅、逃逸閉包和非逃逸閉包
??我們先來看一下什么叫做逃逸(escaping)
。逃逸墨技,是指傳遞給一個函數(shù)的閉包可能會在該函數(shù)返回之后被調(diào)用惩阶。也就是說,閉包逃脫出了接收它作為參數(shù)的函數(shù)作用域扣汪。比如說断楷,像我們上面提到的屬性netWorthChangedHandler
,它就是逃逸的崭别。非逃逸閉包(non-escaping closure)
冬筒,就是指在函數(shù)返回之后不可能被調(diào)用的閉包恐锣。因此,它不可能產(chǎn)生強引用舞痰,也就不需要顯式的使用self
土榴。以函數(shù)參數(shù)形式聲明的閉包默認是非逃逸的,其它場景中的閉包都是逃逸的响牛。
??接下來玷禽,我們通過一個示例來演示一下非逃逸閉包。修改Accountant類中的gained(_: )
方法呀打,給它增加一個閉包參數(shù)(也就是函數(shù)參數(shù)):
// 增加了新的資產(chǎn)
func gained(_ dog: Dog, completionHandler: () -> ()) {
netWorth += dog.price
completionHandler()
}
??這樣一修改矢赁,之前所有調(diào)用gained(_: )
方法的地方肯定會報錯。因此贬丛,來到Person.swift這個類中撩银,修改買了寵物狗方法中的代碼如下:
// 買了寵物狗
func buyDogs(_ dog: Dog) {
// dog.owner = self
// dogs.append(dog)
// 如果有新增的資產(chǎn),需要進行記錄
accountant.gained(dog) {
// 在這個閉包中不需要寫self.dog.owner
// 因為編譯器知道傳遞給函數(shù)gained(_ : completionHandler: )
// 的閉包是非逃逸的豺憔。因為蜒蕾,以函數(shù)參數(shù)形式聲明的閉包默認都是非逃逸的。
// 所有非逃逸閉包都不可能產(chǎn)生強引用焕阿,所以就不需要顯式的使用self了
dog.owner = self
dogs.append(dog)
}
}
??需要特別強調(diào)的是咪啡,在gained(_ : completionHandler: )
這個方法中,參數(shù)completionHandler
是函數(shù)形式(閉包是特殊函數(shù))暮屡,而所有作為函數(shù)參數(shù)形式的閉包撤摸,默認都是非逃逸的,也就是不可能產(chǎn)生強引用褒纲,為此也就是不需要顯式的使用關(guān)鍵字self
了准夷。
??那么,又該如何告訴編譯器此閉包是逃逸的呢莺掠?修改Person.swift中的代碼衫嵌,給它新增一個方法:
func useNetWorthChangedHandler(handler: @escaping (Double) -> ()) {
// 將閉包參數(shù)handler賦值給Accountant類的屬性netWorthChangedHandler,
// 而Accountant類的netWorthChangedHandler是存儲屬性彻秆,這也就是意味著
// 在函數(shù)返回之后需要調(diào)用它楔绞,也就是說閉包是會逃脫函數(shù)的作用域的。而閉包參數(shù)
// 在函數(shù)中默認是非逃逸的唇兑,為此酒朵,需要用@escaping告訴編譯器,handler是非逃逸的
accountant.netWorthChangedHandler = handler
}
??將一個閉包作為參數(shù)傳遞給函數(shù)扎附,并且告訴編譯器蔫耽,這個閉包參數(shù)是逃逸的,這種用法在項目中存在著廣泛的用途留夜,應(yīng)該要掌握匙铡。比如說图甜,我之前的項目QTRadio,請求網(wǎng)絡(luò)數(shù)據(jù)的方法中就用到了逃逸閉包:
extension NavBarViewModel {
/// 請求網(wǎng)絡(luò)數(shù)據(jù)并將其轉(zhuǎn)換為模型
func requestData(completionHandler: @escaping () -> ()) {
// 通過Alamofrie來發(fā)送網(wǎng)絡(luò)請求
NetworkTools.shareTools.requestData(kRequestURL, .get, parameters: ["wt": "json", "v": "6.0.4", "deviceid": "093e8b7e24c02246fe92373727e4a92c", "phonetype": "iOS", "osv": "11.1.1", "device": "iPhone", "pkg": "com.Qting.QTTour"]) { (result) in
/// 將JSON數(shù)據(jù)轉(zhuǎn)成字典
guard let resultDict = result as? [String: Any] else { return }
/// 根據(jù)字典中的關(guān)鍵字data取出字典中的數(shù)組數(shù)據(jù)
guard let resultArray = resultDict["data"] as? [[String: Any]] else { return }
/// 遍歷數(shù)組resultArray鳖眼,取出它里面的字典
for dict in resultArray {
// 將字典轉(zhuǎn)為模型
let item = NavBarModel(dict: dict)
// 將轉(zhuǎn)換完成的模型存儲起來
self.navBarModelArray.append(item)
}
// 數(shù)據(jù)回調(diào)
completionHandler()
}
}
}
??詳細代碼參見ios-step-by-step黑毅,如果有好的修改建議,請給我留言具帮,本人將十分感謝博肋。