此文選自*標(biāo)哥博客*http://www.huangyibiao.com/archives/55
前言:
Swift使用自動(dòng)引用計(jì)數(shù)(ARC)機(jī)制來處理內(nèi)存。通常情況下韧拒,Swift內(nèi)存管理機(jī)制會(huì)自動(dòng)管理內(nèi)存掖看,無須我們考慮內(nèi)存的管理踱卵。ARC會(huì)在類的實(shí)例不再被使用(也就是沒有引用)時(shí)月趟,會(huì)自動(dòng)釋放其占用的內(nèi)存。
可是颜骤,在少數(shù)情況下,ARC需要更多地了解我們代碼之間的聯(lián)系捣卤,才能正確管理內(nèi)存复哆。本篇文章就這少數(shù)情況而討論和分析其應(yīng)用場景及如何更好地解決循環(huán)引用的問題欣喧。
注意:ARC僅應(yīng)用于類的實(shí)例。結(jié)構(gòu)體和枚舉類型是值類型梯找,不是引用類型唆阿,也不是通過引用的方式存儲(chǔ)和傳遞。
ARC的工作機(jī)制
創(chuàng)建類實(shí)例時(shí)锈锤,該實(shí)例就擁有了一塊內(nèi)存驯鳖,存儲(chǔ)相關(guān)信息,且會(huì)使該實(shí)例的引用計(jì)數(shù)+1久免,如下面的person實(shí)例當(dāng)前的引用計(jì)數(shù)就為1浅辙,當(dāng)前person對(duì)象只有一個(gè)引用。
class Person: NSObject {
deinit {
print("deinit")
}
}
var person: Person? = Person()
如果有多個(gè)引用阎姥,則該實(shí)例就不會(huì)被釋放记舆,看下面的mary和lili都引用了person,使其所指向的內(nèi)存引用計(jì)數(shù)+2:
var mary = person
var lili = person
現(xiàn)在我們將mary,lili設(shè)為nil,則person所指向的內(nèi)存塊引用計(jì)數(shù)-2呼巴,但是并不為0泽腮,因此還不會(huì)釋放。
mary = nil
lili = nil
現(xiàn)在我們再將person設(shè)為nil衣赶,會(huì)如何呢诊赊?自然會(huì)得到釋放了,因此就打印deinit出來了府瞄。
person = nil
為了確保使用中的實(shí)例不會(huì)被銷毀碧磅,ARC會(huì)跟蹤和計(jì)算每一個(gè)實(shí)例正在被多少屬性,常量和變量所引用遵馆。哪怕實(shí)例的引用數(shù)為1鲸郊,ARC都不會(huì)銷毀這個(gè)實(shí)例。
無論將實(shí)例賦值給屬性货邓、常量或變量严望,它們都會(huì)創(chuàng)建此實(shí)例的強(qiáng)引用。之所以稱之為“強(qiáng)”引用逻恐,是因?yàn)樗鼤?huì)將實(shí)例牢牢的保持住像吻,只要強(qiáng)引用還在,實(shí)例是不允許被銷毀的复隆。
場景一:類實(shí)例間的循環(huán)強(qiáng)引用
下面看一種這么一種情形:人可以有公寓拨匆,公寓可以屬于某個(gè)人。
class Person: NSObject {
var name: String
// 不一定人人都有公寓挽拂,因此設(shè)置為可選類型更合適
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("deinit person: \(self.name)")
}
}
class Apartment: NSObject {
var unit: String
// 公寓也可能還沒有擁有者惭每,設(shè)置為可選類型
// 注意,弱引用只能聲明為變量,不能聲明為常量
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)在將這兩個(gè)對(duì)象的指向都修改為nil:
// 現(xiàn)在釋放台腥,然后就會(huì)得到打印結(jié)果:
// deinit person: Mary
// deinit apartment: yaya
mary = nil
yaya = nil
說明這兩個(gè)對(duì)象都得到正確的釋放了宏赘。這與我們預(yù)期的是一致的。
下面換一種寫法:如果我們將var apartment: Apartment?也聲明為weak呢黎侈?也就是改成weak var apartment: Apartment?察署,現(xiàn)在我們只將yaya改為nil會(huì)如何呢?
yaya = nil
print(mary?.apartment?.unit)
當(dāng)apartment聲明為var apartment: Apartment?峻汉,其打印結(jié)果為Optional("yaya")
當(dāng)apartment聲明為weak var apartment: Apartment?贴汪,其打印結(jié)果為nil。
雖然這么做兩者都得到釋放休吠,但是這跟我們預(yù)期的結(jié)果不一致扳埂。這是因?yàn)殡p方都是弱引用,當(dāng)yaya =nil時(shí)瘤礁,其引用計(jì)數(shù)值為0阳懂,因此會(huì)被釋放。這時(shí)候再去訪問柜思,就不存在了岩调。如果其類型不是可選類型,釋放后再訪問酝蜒,會(huì)導(dǎo)致crash的。
如果兩個(gè)類實(shí)例之間都是可選類型矾湃,使用weak關(guān)鍵字來處理弱引用更好亡脑,但應(yīng)該是一強(qiáng)一弱引用。
場景二:類實(shí)例與屬性的循環(huán)強(qiáng)引用
下面的例子定義了兩個(gè)類邀跃,Customer和CreditCard霉咨,模擬了銀行客戶和客戶的信用卡。這兩個(gè)類中拍屑,每一個(gè)都將另外一個(gè)類的實(shí)例作為自身的屬性途戒。這種關(guān)系可能會(huì)造成循環(huán)強(qiáng)引用
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)鍵字聲明為無主引用,可以聲明為變量或者常量僵驰,但是不能聲明為可選類型
// 對(duì)于無主引用喷斋,永遠(yuǎ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é)果粉私,我們可以看到這兩個(gè)實(shí)例都得到正確的釋放了顽腾。使用unowned關(guān)鍵字聲明為無主引用,可以聲明為變量或者常量诺核,但是不能聲明為可選類型抄肖。對(duì)于無主引用久信,永遠(yuǎn)都是有值的,因此不可聲明為可選類型漓摩。
注意: 如果試圖在實(shí)例被銷毀后裙士,訪問該實(shí)例的無主引用,會(huì)觸發(fā)運(yùn)行時(shí)錯(cuò)誤幌甘。使用無主引用潮售,你必須確保引用始終指向一個(gè)未銷毀的實(shí)例。還需要注意的是,如果試圖訪問實(shí)例已經(jīng)被銷毀的無主引用锅风,Swift確保程序會(huì)直接崩潰酥诽,而不會(huì)發(fā)生無法預(yù)期的行為。
場景三:閉包引起的循環(huán)強(qiáng)引用
下面我們定義DemoView和DemoController類皱埠,前者有一個(gè)閉包作為屬性肮帐,用于反向傳值到后者。
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)训枢,強(qiáng)引用了self,也就是DemoController
print("\(self.name)")
}
// 測試調(diào)用
self.passToDmeoView(true)
}
func passToDmeoView(selected: Bool) {
demoView?.callback(selected)
}
deinit {
print("DemoController deinit")
}
}
對(duì)比一下添加了[unowned self]與未添加的打印結(jié)果:
不添加[unowned self]忘巧,直接使用self.name時(shí)恒界,當(dāng)返回上一個(gè)界面時(shí),由于循環(huán)強(qiáng)引用砚嘴,DemoController對(duì)象和DemoView者得不到釋放十酣。
添加了[unowned self]后,打印結(jié)果說明可以得到釋放际长,如下:
DemoController deinit
demo view deinit
我們在使用閉包時(shí)耸采,一定要注意循環(huán)強(qiáng)引用的問題,否則很容易千萬內(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)強(qiáng)引用如绸,對(duì)于weak delegate = self.delegate!也需要使用弱引用嘱朽。
對(duì)于閉包內(nèi)的引用,何時(shí)使用弱引用怔接,何時(shí)使用無主引用呢燥翅?
在閉包和捕獲的實(shí)例總是互相引用時(shí)并且總是同時(shí)銷毀時(shí),將閉包內(nèi)的捕獲定義為無主引用蜕提,如上面的closure閉包中的self與closure問題互相引用且同時(shí)銷毀森书。
在被捕獲的引用可能會(huì)變?yōu)閚il時(shí),將閉包內(nèi)的捕獲定義為弱引用。弱引用總是可選類型凛膏,并且當(dāng)引用的實(shí)例被銷毀后杨名,弱引用的值會(huì)自動(dòng)置為nil。
注意:如果被捕獲的引用絕對(duì)不會(huì)變?yōu)閚il猖毫,應(yīng)該用無主引用台谍,而不是弱引用。如果上面的self.delegate不是可選類型吁断,那么其值會(huì)一直存在趁蕊,我們就不應(yīng)該使用weak引用,而是使用unowned引用仔役。
總結(jié)
解決循環(huán)強(qiáng)引用的類型有兩種:
使用weak關(guān)鍵字聲明為弱引用掷伙。使用weak的弱引用只能聲明為變量(不能聲明為常量),表明其值能在運(yùn)行時(shí)被修改又兵。
使用unowned關(guān)鍵聲明為無主引用任柜。無主引用總是被定義為非可選類型,因?yàn)槠溆肋h(yuǎn)是有值的沛厨,可聲明為常量或變量宙地,值不能為nil。
常見的循環(huán)強(qiáng)引用場景有:類實(shí)例與類屬性間循環(huán)強(qiáng)引用逆皮、類實(shí)例與類實(shí)例間循環(huán)強(qiáng)引用绰精、類與閉包循環(huán)強(qiáng)引用等