之前用Swift寫(xiě)了一個(gè)App,已經(jīng)在App Store上架了。前兩天更新了一些功能,然后用Instruments檢查的時(shí)候,發(fā)現(xiàn)有內(nèi)存泄漏問(wèn)題。有些同學(xué)可能覺(jué)得奇怪,Swift不是使用ARC自動(dòng)管理內(nèi)存的么,怎么也會(huì)發(fā)生內(nèi)存泄漏呢。是會(huì)的,但幾乎都是由于操作不當(dāng)造成循環(huán)引用(strong reference cycle/retain cycle)導(dǎo)致的。
ARC與GC
很多人分不清ARC(Automatic Reference Counting,自動(dòng)引用計(jì)數(shù))跟GC(Garbage Collection,垃圾收集)的區(qū)別。其實(shí)“引用計(jì)數(shù)法”也算是一種GC策略,只不過(guò)我們現(xiàn)在提到GC的時(shí)候一般是指基于“標(biāo)記-整理”策略的垃圾收集器,譬如主流的JVM(Java虛擬機(jī))幾乎都是采用“標(biāo)記-整理”+“分代收集”的策略來(lái)進(jìn)行自動(dòng)內(nèi)存管理的。標(biāo)記算法一般是從全局對(duì)象圖的“根”出發(fā)進(jìn)行可達(dá)性分析,對(duì)象的生死會(huì)被批量地標(biāo)記出來(lái),之后再在某個(gè)時(shí)間批量地釋放死對(duì)象。顯然,這是一種“全局+延時(shí)”的管理策略。
而與之相對(duì)的,引用計(jì)數(shù)是一種“局部+即時(shí)”的內(nèi)存管理策略。它不需要全局的對(duì)象信息,一般每個(gè)被管理的對(duì)象都會(huì)跟一個(gè)引用計(jì)數(shù)器關(guān)聯(lián),這個(gè)計(jì)數(shù)器保存著當(dāng)前對(duì)象被引用的次數(shù),一旦創(chuàng)建一個(gè)新的引用指向該對(duì)象,引用計(jì)數(shù)就加1,每當(dāng)指向該對(duì)象的某個(gè)引用失效引用計(jì)數(shù)就減1,直到引用計(jì)數(shù)為0,就立即釋放該對(duì)象。使用引用計(jì)數(shù)法管理內(nèi)存的語(yǔ)言也不止OC和Swift,還有諸如CPython之類的GC也是基于引用計(jì)數(shù)的。
早年OC是采用MRC(手動(dòng)引用計(jì)數(shù))的,當(dāng)然其實(shí)現(xiàn)在也有人還在用,它跟ARC的主要區(qū)別在于它需要手動(dòng)管理引用計(jì)數(shù)器,而ARC是自動(dòng)管理的。所以其實(shí)MRC也不能讓你直接釋放對(duì)象的,只是控制引用罷了。
循環(huán)引用
上面解釋了一下ARC的運(yùn)作方式,從中不難看出這種策略的缺陷,就是循環(huán)引用問(wèn)題??聪聢D:

object1和object2之間形成了循環(huán)引用,它們的引用計(jì)數(shù)始終為1,始終不會(huì)被釋放,這就造成了內(nèi)存泄漏?!皹?biāo)記-整理”策略并不會(huì)出現(xiàn)這種問(wèn)題,因?yàn)槟呐聝蓚€(gè)對(duì)象相互引用,但只要它們和“根”對(duì)象失去了聯(lián)系,照樣會(huì)被標(biāo)記為死對(duì)象,然后在合適的時(shí)間被釋放。
實(shí)例分析
接下來(lái)看一個(gè)稍微復(fù)雜一點(diǎn)的實(shí)例,分析一下出現(xiàn)循環(huán)引用的原因然后給出解決方法。
class SimpleRefreshCtrl: UIRefreshControl {
typealias Action = () -> ()
var action: Action!
init(action: Action) {
super.init()
tintColor = UIColor.navigationBarColor()
self.action = action
self.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func refresh() {
self.action()
delay(seconds: 1) {
self.endRefreshing()
}
}
}
這是我自己封裝的一個(gè)下拉刷新控制器,它繼承自UIRefreshControl,可以在UITableViewController中直接使用,如下:
class HouseTableCtrl: UITableViewController {
//...
func getPageData() {
getListFromApi(urlString) { json, nextLink in
self.houseData = json
self.page = nextLink
}
}
override func viewDidLoad() {
super.viewDidLoad()
let refreshCtrl = SimpleRefreshCtrl(action: getPageData)
self.refreshControl = refreshCtrl
//...
}
}
這樣,當(dāng)你下拉列表的時(shí)候,旋轉(zhuǎn)的菊花就會(huì)出現(xiàn)旋轉(zhuǎn)1秒,同時(shí)執(zhí)行getPageData方法,刷新頁(yè)面數(shù)據(jù)。
但是這里出現(xiàn)了循環(huán)引用問(wèn)題,我們來(lái)看看它是怎么發(fā)生的。在getPageData方法中我調(diào)用了一個(gè)全局函數(shù)getListFromApi,而這個(gè)全局函數(shù)需要一個(gè)閉包作為參數(shù),而這個(gè)閉包又捕獲了當(dāng)前對(duì)象的兩個(gè)屬性,也就持有了當(dāng)前對(duì)象的引用。到這里為止并沒(méi)有什么問(wèn)題,雖然閉包捕獲外部變量從而持有外部對(duì)象的引用經(jīng)常是造成循環(huán)引用的一大元兇,但在這里,該閉包是個(gè)匿名閉包,我們的HouseTableCtrl對(duì)象并沒(méi)有持有該閉包的引用,所以問(wèn)題并不是出在這里。
接下來(lái),在初始化SimpleRefreshCtrl對(duì)象的時(shí)候,getPageData作為參數(shù)被傳遞了過(guò)去,并被賦值給SimpleRefreshCtrl的實(shí)例屬性action。注意,getPageData是在HouseTableCtrl中定義的一個(gè)實(shí)例方法,是跟當(dāng)前的HouseTableCtrl對(duì)象關(guān)聯(lián)的,作為參數(shù)傳遞過(guò)去的實(shí)際上是self.getPageData。如此一來(lái),SimpleRefreshCtrl對(duì)象就持有了當(dāng)前HouseTableCtrl對(duì)象的引用。然后接下來(lái)這一句self.refreshControl = refreshCtrl,持有HouseTableCtrl對(duì)象引用的SimpleRefreshCtrl對(duì)象被賦值給了HouseTableCtrl的實(shí)例屬性refreshControl,于是HouseTableCtrl對(duì)象也持有了SimpleRefreshCtrl對(duì)象的引用。這就造成了循環(huán)引用。
要如何打破僵局呢,其實(shí)也很簡(jiǎn)單,使用weak或者unowned就行了:
//refreshCtrl指向的對(duì)象只持有當(dāng)前HouseTableCtrl對(duì)象的一個(gè)弱引用
let refreshCtrl = SimpleRefreshCtrl { [weak self] in
self?.getPageData()
}
//這一句強(qiáng)引用
self.refreshControl = refreshCtrl
這樣SimpleRefreshCtrl對(duì)象就只是持有當(dāng)前HouseTableCtrl對(duì)象的一個(gè)弱引用,弱引用是不算在HouseTableCtrl對(duì)象的引用計(jì)數(shù)中的,也就是說(shuō)當(dāng)沒(méi)有其他引用指向HouseTableCtrl對(duì)象時(shí),HouseTableCtrl對(duì)象能被正常釋放,一旦HouseTableCtrl對(duì)象被釋放了,那SimpleRefreshCtrl對(duì)象也就能被正常釋放了:

至于weak和unowned該用哪個(gè)么,看情況了,weak修飾的屬性或變量是一個(gè)optional類型,也就是說(shuō)是可以為nil的。而unowned則是修飾一個(gè)nonoptional,是不能為nil的,一旦這個(gè)屬性或變量指向的對(duì)象被釋放了(這是有可能發(fā)生的,因?yàn)閡nowned引用也是不算在引用計(jì)數(shù)中的,如果除了unowned引用外沒(méi)有其他引用指向那個(gè)對(duì)象,那它將被釋放),而你還想使用該對(duì)象的話,將會(huì)觸發(fā)runtime error,程序也就crash了。所以個(gè)人來(lái)說(shuō),我是更推薦使用weak的。