Swift使用自動引用計數(shù)(ARC)機制來處理內(nèi)存咬荷。通常情況下,Swift內(nèi)存管理機制會自動管理內(nèi)存白群,無須我們考慮內(nèi)存的管理盘榨。ARC會在類的實例不再被使用(也就是沒有引用)時,會自動釋放其占用的內(nèi)存。
可是井佑,在少數(shù)情況下属铁,ARC需要更多地了解我們代碼之間的聯(lián)系,才能正確管理內(nèi)存躬翁。本篇文章就這少數(shù)情況而討論和分析其應用場景及如何更好地解決循環(huán)引用的問題焦蘑。
注意:ARC僅應用于類的實例。結(jié)構(gòu)體和枚舉類型是值類型盒发,不是引用類型例嘱,也不是通過引用的方式存儲和傳遞。
ARC的工作機制
創(chuàng)建類實例時宁舰,該實例就擁有了一塊內(nèi)存栗竖,存儲相關(guān)信息谓松,且會使該實例的引用計數(shù)+1碉克,如下面的person實例當前的引用計數(shù)就為1府喳,當前person對象只有一個引用倔毙。
class Person: NSObject {
deinit {
print("deinit")
}
}
var person: Person? = Person()
如果有多個引用勇婴,則該實例就不會被釋放械哟,看下面的mary和lili都引用了person,使其所指向的內(nèi)存引用計數(shù)+2:
var mary = person
var lili = person
現(xiàn)在我們將mary,lili設為nil从撼,則person所指向的內(nèi)存塊引用計數(shù)-2详羡,但是并不為0仍律,因此還不會釋放。
mary = nil
lili = nil
現(xiàn)在我們再將person設為nil实柠,會如何呢水泉?自然會得到釋放了,因此就打印deinit出來了窒盐。
person = nil
為了確保使用中的實例不會被銷毀草则,ARC會跟蹤和計算每一個實例正在被多少屬性,常量和變量所引用蟹漓。哪怕實例的引用數(shù)為1炕横,ARC都不會銷毀這個實例。
無論將實例賦值給屬性葡粒、常量或變量份殿,它們都會創(chuàng)建此實例的強引用。之所以稱之為“強”引用嗽交,是因為它會將實例牢牢的保持住卿嘲,只要強引用還在,實例是不允許被銷毀的夫壁。
場景一:類實例間的循環(huán)強引用
下面看一種這么一種情形:人可以有公寓拾枣,公寓可以屬于某個人。
class Person: NSObject {
var name: String
// 不一定人人都有公寓盒让,因此設置為可選類型更合適
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("deinit person: \(self.name)")
}
}
class Apartment: NSObject {
var unit: String
// 公寓也可能還沒有擁有者放前,設置為可選類型
// 注意忿磅,弱引用只能聲明為變量,不能聲明為常量
weak var owner: Person?
init(unit: String) {
self.unit = unit
}
deinit {
print("deinit apartment: \(self.unit)")
}
}
var mary: Person? = Person(name: "Mary")
var yaya: Apartment? = Apartment(unit: "yaya")
// 下面是建立兩者之間的關(guān)聯(lián)關(guān)系
mary?.apartment = yaya
yaya?.owner = mary
現(xiàn)在將這兩個對象的指向都修改為nil:
// 現(xiàn)在釋放凭语,然后就會得到打印結(jié)果:
// deinit person: Mary
// deinit apartment: yaya
mary = nil
yaya = nil
說明這兩個對象都得到正確的釋放了葱她。這與我們預期的是一致的。
下面換一種寫法:如果我們將var apartment: Apartment?也聲明為weak呢似扔?也就是改成weak var apartment: Apartment?吨些,現(xiàn)在我們只將yaya改為nil會如何呢?
yaya = nil
print(mary?.apartment?.unit)
當apartment聲明為var apartment: Apartment?炒辉,其打印結(jié)果為Optional("yaya")
當apartment聲明為weak var apartment: Apartment?豪墅,其打印結(jié)果為nil。
雖然這么做兩者都得到釋放黔寇,但是這跟我們預期的結(jié)果不一致偶器。這是因為雙方都是弱引用,當yaya =nil時缝裤,其引用計數(shù)值為0屏轰,因此會被釋放。這時候再去訪問憋飞,就不存在了霎苗。如果其類型不是可選類型,釋放后再訪問榛做,會導致crash的唁盏。
如果兩個類實例之間都是可選類型,使用weak關(guān)鍵字來處理弱引用更好检眯,但應該是一強一弱引用厘擂。
場景二:類實例與屬性的循環(huán)強引用
下面的例子定義了兩個類,Customer和CreditCard锰瘸,模擬了銀行客戶和客戶的信用卡刽严。這兩個類中,每一個都將另外一個類的實例作為自身的屬性获茬。這種關(guān)系可能會造成循環(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關(guān)鍵字聲明為無主引用港庄,可以聲明為變量或者常量,但是不能聲明為可選類型
// 對于無主引用恕曲,永遠都是有值的鹏氧,因此不可聲明為可選類型。
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
現(xiàn)在我們將john置為nil佩谣,其結(jié)果如何呢把还?
// 打印結(jié)果:
// John is being deinitialized
// Card #1234567890123456 is being deinitialized
john = nil
根據(jù)打印結(jié)果,我們可以看到這兩個實例都得到正確的釋放了。使用unowned關(guān)鍵字聲明為無主引用吊履,可以聲明為變量或者常量安皱,但是不能聲明為可選類型。對于無主引用艇炎,永遠都是有值的酌伊,因此不可聲明為可選類型。
注意:
如果試圖在實例被銷毀后缀踪,訪問該實例的無主引用居砖,會觸發(fā)運行時錯誤。使用無主引用驴娃,你必須確保引用始終指向一個未銷毀的實例奏候。還需要注意的是,如果試圖訪問實例已經(jīng)被銷毀的無主引用,Swift確保程序會直接崩潰唇敞,而不會發(fā)生無法預期的行為蔗草。
場景三:閉包引起的循環(huán)強引用
下面我們定義DemoView和DemoController類,前者有一個閉包作為屬性疆柔,用于反向傳值到后者咒精。
typealias DemoClosure = (isSelected: Bool) ->Void
class DemoView: UIView {
var closure: DemoClosure?
func callback(selected: Bool) {
if let callback = closure {
callback(isSelected: selected)
}
}
deinit {
print("demo view deinit")
}
}
class DemoController: UIViewController {
var demoView: DemoView?
var name: String = "DemoControllerName"
override func viewDidLoad() {
super.viewDidLoad()
demoView = DemoView()
self.view.addSubview(demoView!)
// 注意,這里使用了[unowned self]無主引用
demoView?.closure = { [unowned self](isSelcted: Bool) in
// 閉包內(nèi)婆硬,強引用了self狠轻,也就是DemoController
print("\(self.name)")
}
// 測試調(diào)用
self.passToDmeoView(true)
}
func passToDmeoView(selected: Bool) {
demoView?.callback(selected)
}
deinit {
print("DemoController deinit")
}
}
對比一下添加了[unowned self]與未添加的打印結(jié)果:
不添加[unowned self]奸例,直接使用self.name時彬犯,當返回上一個界面時,由于循環(huán)強引用查吊,DemoController對象和DemoView者得不到釋放谐区。
添加了[unowned self]后,打印結(jié)果說明可以得到釋放逻卖,如下:
DemoController deinit
demo view deinit
我們在使用閉包時宋列,一定要注意循環(huán)強引用的問題,否則很容易千萬內(nèi)存泄露评也。除了使用無主引用之外炼杖,還有weak關(guān)鍵字來聲明使用弱使用,比如
lazy var closure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate] (index: Int, title: String) -> String in
// closure body goes here
print("\(self.name)")
delegate.callBack(title)
}
為了防止循環(huán)強引用盗迟,對于weak delegate = self.delegate!也需要使用弱引用坤邪。
對于閉包內(nèi)的引用,何時使用弱引用罚缕,何時使用無主引用呢艇纺?
在閉包和捕獲的實例總是互相引用時并且總是同時銷毀時,將閉包內(nèi)的捕獲定義為無主引用,如上面的closure閉包中的self與closure問題互相引用且同時銷毀黔衡。
在被捕獲的引用可能會變?yōu)閚il時蚓聘,將閉包內(nèi)的捕獲定義為弱引用。弱引用總是可選類型盟劫,并且當引用的實例被銷毀后夜牡,弱引用的值會自動置為nil。
注意:如果被捕獲的引用絕對不會變?yōu)閚il侣签,應該用無主引用氯材,而不是弱引用。如果上面的self.delegate不是可選類型硝岗,那么其值會一直存在氢哮,我們就不應該使用weak引用,而是使用unowned引用型檀。