swift使用自動引用計數(shù)(ARC)機制來跟蹤和管理應用程序的內存。一般情況下,swift內存管理機制會一直起作用,即開發(fā)者無需考慮內存管理。ARC會在類的實例不再使用時,即沒有引用的時候,自動釋放其所占用的內存。
?但是要注意,引用計數(shù)僅僅應用在類的實例,因為結構體和枚舉類型是值類型,不是引用類型,也不是通過引用的方式存儲和傳遞的。
一、自動引用計數(shù)的工作機制
當在創(chuàng)建一個類的實例時,ARC會分配一個內存用于存儲實例的信息。內存中會包含實例的類型信息,以及這個實例中所有相關屬性的值。
?當實例不再被使用時,ARC會釋放該實例所占用的內存,并讓釋放的內存挪作他用。這即是確保了不再被使用的實例,不會一直占用內存空間。
?為了確保使用中的實例不會被銷毀,ARC會跟蹤和計算每個實例正在被多少屬性、變量和常量所引用,只要是實例還有被引用,ARC就不會銷毀該實例。
不論是將實例賦值給屬性、常量還是變量,它們都會創(chuàng)建對此實例的強引用。強引用就是將實例牢牢的保持住,只要是有強引用,實例就不會被銷毀!!!
二、自動引用計數(shù)實踐
以下是展示自動引用計數(shù)的工作機制:
class Student {
let name:String
init(name:String) { // 構造器
self.name = name;
print("名字的初始值為:\(name)");
}
deinit { // 析構器
print("實例將會被釋放 --- \(name)");
}
}
// 定義三個學生變量,類型是Student?,注意是可選類型
var student1:Student?;
var student2:Student?;
var student3:Student?;
// 實例化,注意這里就是一個強引用
// // 此時Student實例強引用計數(shù)為1
student1 = Student(name: "張三");
print("學生姓名: \(student1!.name)");
// 賦值操作
student2 = student1; // 此時Student實例強引用計數(shù)為2
student3 = student1; // 此時Student實例強引用計數(shù)為3
// 通過設置變量為`nil`,即是斷開強引用
student1 = nil; // 此時Student實例強引用計數(shù)為2
student2 = nil; // 此時Student實例強引用計數(shù)為1
// 注意看打印效果,此時Student實例是沒有被使用的,因為`deinit`析構器中代碼還沒被調用
// 當student3設置為`nil`時,此時Student實例強引用計數(shù)為0
student3 = nil;
// 當沒有強引用的時候,才會被銷毀
輸出結果:
名字的初始值為:張三
學生姓名: 張三
實例將會被釋放 --- 張三
三、類實例之間引起的循環(huán)引用
如果兩個類實例相互持有對方的強引用,這即是循環(huán)引用。解決辦法就是通過定義類之間關系為弱引用或無主引用,以代替強引用(下面會有實際解決實例,這里先了解循環(huán)引用是怎么產(chǎn)生的)。
/**
Student學生類: 具體某個學生,哪個班級
Grade班級類: 具體班級,班級里的學生有誰
*/
class Student {
// 學生名字
let name:String
// 構造器
init(name:String) {
self.name = name
}
// 哪個班級
var grade:Grade?
// 析構器
deinit {
print("實例將會被釋放 --- \(name)");
}
}
class Grade {
// 哪個班級
let gradeName:String
//構造器
init(gradeName:String) {
self.gradeName = gradeName
}
// 班級中的學生
var student:Student?
// 析構器
deinit {
print("實例將會被釋放 --- \(gradeName)");
}
}
// 學生: 張三
var student:Student? = Student(name: "張三")
// 班級: 一年一班
var grade:Grade? = Grade(gradeName: "一年一班")
// 張三是在一年一班
student!.grade = grade
// 一年一班中的學生
grade!.student = student
// 設置為`nil`,因為它們都是可選類型的
student = nil
grade = nil
// 但是Student和Grade的析構器都沒調用,說明它們的實例是沒有被釋放
開始,實例化操作,那么此時對應類的實例都只有一個強引用所指向(圖中實線就表示強引用):
之后,是賦值操作
student!.grade = grade
和grade!.student = student
,此時:最后,設置
student = nil
和grade = nil
,此時:從上可以看到,
Student
和Grade
的實例都是有強引用,即實例不會被釋放掉,那么這也就會造成內存的泄漏。
四、解決實例之間的循環(huán)引用
swift提供了兩種辦法用來解決循環(huán)引用的問題:弱引用和無主引用。
?弱引用和無主引用允許循環(huán)引用中的一個實例引用和另一個實例而不是保持強引用,這也就不會產(chǎn)生循環(huán)引用問題。
?對于生命周期中會為nil
的實例使用弱引用。但對于初始化值后不會再被賦值為nil
的實例,則使用無主引用。
-
弱引用,是不會阻止ARC銷毀被引用的實例,或者說弱引用是不計數(shù)是不會加一操作的。當聲明屬性或變量時,在前面加上
weak
關鍵字表明一個弱引用。
// 注: 與上面代碼一樣,只是在定義student屬性時,進行了弱化操作
class Grade {
// 哪個班級
let gradeName:String
// 構造器
init(gradeName:String) {
self.gradeName = gradeName
}
// 班級中的學生
// var student:Student?
// 弱引用操作,解決實例之間的循環(huán)引用
weak var student:Student?
// 析構器
deinit {
print("實例將會被釋放 --- \(gradeName)");
}
}
當設置
student = nil
和grade = nil
的時候(注意圖中標注的先后順序。另外只要實例沒有強引用,那么會被釋放):注意1: 弱引用必須被聲明為變量,表明其值能在運行時被修改!!!另外,弱引用可以沒有值,所以也必須將弱引用聲明為可選類型。而swift中也是推薦使用可選類型來描述可能沒有值的類型。
注意2: 在使用垃圾收集的系統(tǒng)里,弱指針有時候來實現(xiàn)簡單的緩沖機制,因為沒有強引用的對象只會在內存壓力觸發(fā)垃圾收集時才會被銷毀。但ARC中,一旦值的最后一個強引用被刪除,就會被立即銷毀,這導致弱引用并不適合上面的用途。
-
無主引用,與弱引用類似,無主引用也不會牢牢保持住引用的實例。但與弱引用不同的是,無主引用永遠是有值的!!!因此無主引用總是被定義為非可選類型。而在聲明屬性或變量時,在前面加上
unowned
關鍵字來表示是一個無主引用。由于無主引用是非可選類型,你不需要再使用它的時候將它展開,無主引用總是可以被直接訪問。但ARC無法再實例被實例銷毀后將無主引用設置為nil
,因為非可選類型的變量不允許被賦值為nil
。
注意: 如果你試圖在實例被銷毀后,訪問該實例的無主引用,會導致程序崩潰。使用無主引用,必須要確保引用始終指向一個未銷毀的實例。
/** 無主引用
例如銀行顧客Customer和信用卡CreditCard;
一個人可以有或沒有信用卡,但一張信用卡必須與一個客戶關聯(lián);
*/
// 銀行顧客
class Customer {
// 用戶名
let name:String
// 用戶對應的信用卡,可選類型表示可以有卡,也可以沒有卡
var card:CreditCard?
init(name:String) {
self.name = name
}
deinit {
print("銀行顧客-實例被銷毀 --- \(self.name)")
}
}
// 信用卡
class CreditCard {
// 卡號
let number:UInt64
// 無主引用,避免循環(huán)引用(非可選類型,信用卡實例必須與一個客戶關聯(lián))
unowned let customer:Customer
init(number:UInt64, customer:Customer) {
self.number = number
self.customer = customer
}
deinit {
print("信用卡-實例被銷毀 --- \(self.number)")
}
}
// 銀行客戶,張三(可選類型,是可以設置為`nil`)
var zhangsan:Customer? = Customer(name:"張三");
// 張三的信用卡【注意,初始化的時候將卡號與張三實例傳進去】
zhangsan!.card = CreditCard(number: 1234_5678_9101_1121,customer: zhangsan!)
// 設置為空,即斷開對Customer實例的引用,檢查是否會正常釋放
zhangsan = nil
輸出結果:
銀行顧客-實例被銷毀 --- 張三
信用卡-實例被銷毀 --- 1234567891011121
銀行客戶與信用卡綁定后,它們之間的關系:
之后,設置
zhangsan = nil
,此時Customer實例張三沒有強引用,那么會被釋放。而Customer實例張三釋放以后,CreditCard實例也就沒有強引用,也會被釋放,如圖所示:- 無主引用以及隱式解析可選屬性。
Student
和Grade
示例中,兩個屬性的值都允許為nil
,并存在循環(huán)引用,這種場景最適合弱引用解決;Customer
和CreditCard
示例中,一個屬性允許設置為nil
,而另外一個屬性是不允許為nil
,并存在循環(huán)引用,這種常見最適合無主引用解決;而當兩個屬性都必須有值,并初始化完成后將都不會為nil
,這種場景需要一個類使用無主屬性,另外一個類使用隱式解析可選屬性來解決:
/**
無主引用以及隱式解析可選屬性
Country國家和City城市;
每個國家必須有首都,而每個城市必須屬于一個國家;
*/
// 國家類
class Country {
// 國家名
let name:String
// 首都城市,是隱式解析可選類型(默認是為空,這是不展開都可使用)
var capitalCity:City!
init(name:String, capitalName:String) {
// 指定國家名
self.name = name
// 指定首都城市,傳入城市名以及國家實例
self.capitalCity = City(name:capitalName, country: self)
}
deinit {
print("Country實例被釋放 --- \(name)")
}
}
// 城市類
class City {
// 城市名
let name:String
// 所屬首都,是無主類型
unowned let country:Country
init(name:String, country:Country) {
self.name = name
self.country = country
}
deinit {
print("City實例被釋放 --- \(name)")
}
}
var country = Country(name: "中國", capitalName: "北京")
print("\(country.name) - \(country.capitalCity.name)")
輸出結果:
中國 - 北京
五、閉包引起的循環(huán)引用
在閉包賦值給類實例的某個屬性時也會有強引用。閉包體中可能訪問實例的某個屬性,例如self.someProperty
,或者閉包調用了實例中的某個方法,例如self.someMethod
,這都會導致閉包"捕獲"self
,從而產(chǎn)生了循環(huán)引用:
/**
閉包的循環(huán)引用
*/
class Person {
// 名字
let name:String
// 懶加載,自我介紹
lazy var speakStr:Void -> String = {
// 閉包中
return "大家好,我叫\(zhòng)(self.name)"
}
init(name:String) {
self.name = name
}
deinit {
print("Person實例釋放 --- \(name)");
}
}
// 初始化
var zhangsan:Person? = Person(name: "張三")
// 調用
print(zhangsan!.speakStr())
// 斷開對Person實例的引用,但此時Person實例不會被銷毀
zhangsan = nil
六、解決閉包引起的循環(huán)引用
對于閉包所引起的循環(huán)引用,在swift中提供了一種解決辦法,即是閉包捕獲列表。在定義閉包同時定義捕獲列表作為閉包的一部分,通過這種方式可以解決閉包和類實例之間的循環(huán)引用。捕獲列表定義了閉包體內捕獲一個或多個引用類型的規(guī)則,這與解決兩個類之間的循環(huán)引用一樣,聲明每個捕獲的引用為弱引用或無主引用,而不是強引用。
?在閉包中需要注意的是,只要在閉包中要使用本對象中的屬性或方法,就必須要使用self.someProperty
或self.someMethod()
,而不能使用someProperty
或someMethod()
。
?定義捕獲列表,在捕獲列表中的每一項都由需要加上weak
或unowned
關鍵字。
?在閉包和捕獲的實例總是相互引用時并且是同時銷毀時,可以將閉包內的捕獲定義為無主引用。
?而在被捕獲的引用可能會為nil
時,將閉包內的捕獲定義為弱引用。弱引用總是可選類型,并且當引用的實例被銷毀后,弱引用的值會自動置為nil
。
?如果是被捕獲的引用絕對不會變?yōu)?code>nil,應該用無主引用,而不是引用。
// 閉包有參數(shù)列表和返回類型的定義,把捕獲列表放在前面
lazy var someClosure:(Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index:Int ,stringToProcess:String) -> String in
// 閉包中對應操作
}
// 閉包沒有指定參數(shù)列表或返回類型,即會通過上下文推斷,那么可以把捕獲列表和關鍵字`in`放在閉包最開始地方
lazy var someClosure:Void -> String = {
[unowned self, weak delegate = self.delegate!] in
// 閉包中對應操作
}
class Person {
// 名字
let name:String
// 懶加載,自我介紹
lazy var speakStr:Void -> String = {
// 解決循環(huán)引用,即表示"用無主引用而不是強引用來捕獲self"
[unowned self] in
// 閉包中
return "大家好,我叫\(zhòng)(self.name)"
}
init(name:String) {
self.name = name
}
deinit {
print("Person實例釋放 --- \(name)");
}
}
// 初始化
var zhangsan:Person? = Person(name: "張三")
// 調用
print(zhangsan!.speakStr())
// 斷開對Person實例的引用,此時Person實例就會被銷毀
zhangsan = nil
輸出結果:
大家好,我叫張三
Person實例釋放 --- 張三