看過不少分析Swift解決循環(huán)引用的文章,分析weak和unowned的區(qū)別等等,可能是不太符合我的思路等限,一直感覺很模糊,在平時(shí)使用的時(shí)候?qū)κ裁磿r(shí)候用weak芬膝,什么時(shí)候用unowned方面還是不太明確望门,干脆自己在這方面進(jìn)行了一次整理。
自動(dòng)引用計(jì)數(shù)(ARC)
Swift和OC一樣锰霜,使用的是自動(dòng)引用計(jì)數(shù)的機(jī)制來追蹤和管理APP的內(nèi)存筹误。顧名思義,自動(dòng)引用計(jì)數(shù)是自動(dòng)進(jìn)行的锈遥,并不需要我們手動(dòng)去參與內(nèi)存的管理——當(dāng)一個(gè)實(shí)例使用完了的時(shí)候纫事,會(huì)自動(dòng)對(duì)其占用的內(nèi)存進(jìn)行釋放。當(dāng)然所灸,ARC管理的只是引用類型丽惶,值類型的(比如結(jié)構(gòu)體和枚舉)不在其管理范圍之內(nèi)。
ARC其實(shí)就干了三件事:
- 為新創(chuàng)建的實(shí)例分配內(nèi)存
- 確保使用中的實(shí)例不會(huì)被銷毀
- 確保使用完的實(shí)例被正確釋放爬立,騰出占用的內(nèi)存空間
上面三板斧的實(shí)現(xiàn)是靠ARC維護(hù)一個(gè)計(jì)數(shù)來實(shí)現(xiàn)的钾唬,當(dāng)初始化的時(shí)候,引用計(jì)數(shù)為1侠驯;每次有新的對(duì)這個(gè)實(shí)例的引用的時(shí)候抡秆,引用計(jì)數(shù)加1;每次對(duì)應(yīng)引用被置為nil時(shí)吟策,引用計(jì)數(shù)減1儒士;當(dāng)引用計(jì)數(shù)為0的時(shí)候,該實(shí)例被銷毀檩坚,回收內(nèi)存空間着撩。
舉個(gè)例子吧诅福,假如有一個(gè)類如下:
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name)被注銷了") }
}
這個(gè)類很簡單,一個(gè)name的屬性拖叙,一個(gè)構(gòu)造函數(shù)氓润,一個(gè)析構(gòu)函數(shù)。創(chuàng)建該類的新實(shí)例的時(shí)候薯鳍,調(diào)用構(gòu)造函數(shù)咖气,銷毀該實(shí)例的時(shí)候,調(diào)用析構(gòu)函數(shù)挖滤。
下面崩溪,我們創(chuàng)建一個(gè)Person類的實(shí)例,如下
Person.init(name: "Ivan")
我們只是創(chuàng)建了這個(gè)實(shí)例壶辜,在正常使用中我們是不會(huì)單純這樣做的悯舟,沒有意義。我們會(huì)把這個(gè)實(shí)例賦值給某個(gè)變量來進(jìn)行使用砸民。
let person1 = Person.init(name: "Ivan") // 引用計(jì)數(shù)加1,現(xiàn)在為1
這時(shí)候奋救,person1和這個(gè)Person類的新實(shí)例直接建立了一個(gè)強(qiáng)引用岭参,該實(shí)例的引用計(jì)數(shù)加1。也是因?yàn)樵搶?shí)例有強(qiáng)引用尝艘,所以它所在的內(nèi)存空間不會(huì)被銷毀演侯,在ARC眼中它還有利用價(jià)值。
假如這個(gè)實(shí)例也賦值給了其他變量背亥,如下
let person2 = person1 // 引用計(jì)數(shù)加1秒际,現(xiàn)在為2
let person3 = person1 // 引用計(jì)數(shù)加1,現(xiàn)在為3
let person4 = Person.init(name: "Jack") // 引用計(jì)數(shù)加1狡汉,這是個(gè)新的實(shí)例娄徊,這個(gè)實(shí)例引用計(jì)數(shù)現(xiàn)在為1
當(dāng)變量對(duì)這個(gè)實(shí)例的引用被銷毀,即置為nil的時(shí)候盾戴,就會(huì)減少這個(gè)實(shí)例的引用計(jì)數(shù)寄锐,當(dāng)引用計(jì)數(shù)為0 的是,這個(gè)實(shí)例即被銷毀尖啡,回收內(nèi)存空間橄仆。
person1 = nil // 引用計(jì)數(shù)減1,現(xiàn)在為2
person2 = nil // 引用計(jì)數(shù)減1衅斩,現(xiàn)在為1
person3 = nil // 引用計(jì)數(shù)減1盆顾,現(xiàn)在Person類的這個(gè)實(shí)例被銷毀了
但是ARC畢竟不是智能的,默認(rèn)它會(huì)把所有的引用歸為強(qiáng)引用畏梆,只要還在被其他的屬性您宪、常量惫搏、變量所使用,它是不會(huì)被釋放的蚕涤。但是凡事總有特殊情況筐赔,這時(shí)候就需要對(duì)ARC釋放內(nèi)存的方式進(jìn)行提示(weak,unowned)揖铜。
循環(huán)引用
墨菲定律:如果事情有變壞的可能茴丰,不管這種可能性有多小,它總會(huì)發(fā)生天吓。
我們?cè)倥e一個(gè)例子贿肩,有下面2個(gè)相關(guān)類:
class Person {
let name: String
var pet: Dog?
init(name: String) { self.name = name }
deinit { print("\(name)被注銷了") }
}
class Dog {
let nickName: String
let owner: Person?
init(species: String) { self.species = species }
deinit { print("\(nickName)被注銷了") }
}
發(fā)現(xiàn)Person類多了一個(gè)Dog屬性,Dog類里面也有一個(gè)Person屬性龄寞。一個(gè)人可以擁有一只寵物狗汰规,一只狗也可以擁有一個(gè)主人;同時(shí)因?yàn)橐粋€(gè)人也可以沒有寵物物邑,一只狗也可以是一只野狗溜哮,所以這兩個(gè)變量都是可選的。
那么問題來了:假如我們同時(shí)創(chuàng)建了這兩個(gè)類的實(shí)例并且賦值給了兩個(gè)變量會(huì)怎么樣色解?
var ivan = Person.init(name: "ivan")
var wawa = Dog.init(nickName: "wawa")
就像上圖一樣茂嗓,ivan變量建立了對(duì)Person實(shí)例的強(qiáng)引用,wawa建立了對(duì)Dog實(shí)例的強(qiáng)引用科阎。其實(shí)沒什么述吸,因?yàn)閮烧卟]有什么關(guān)系。但是锣笨,如果我們加上下面的語句:
ivan.pet = wawa
wawa.owner = ivan
那么一切都不一樣了蝌矛,如下圖:
此時(shí)在之前兩個(gè)強(qiáng)引用的基礎(chǔ)上,多了Person實(shí)例中的pet變量對(duì)Dog實(shí)例的強(qiáng)引用错英,以及Dog實(shí)例的owner變量對(duì)Person實(shí)例的強(qiáng)引用入撒。
如果這時(shí)候,我們結(jié)束了對(duì)這兩個(gè)實(shí)例的使用走趋,想要銷毀它們來騰出內(nèi)存空間衅金,這時(shí)候就出問題了。
ivan = nil
wawa = nil
如上圖簿煌,我們的變量到實(shí)例直接的引用已經(jīng)沒有了氮唯,但是這兩個(gè)實(shí)例會(huì)被銷毀嗎?答案是否定的姨伟。因?yàn)樗麄冎苯舆€相互存在引用惩琉,只要還有對(duì)實(shí)例的引用,那么實(shí)例就不會(huì)輕易被銷毀夺荒,內(nèi)存空間也不會(huì)被正確釋放瞒渠,這就是因?yàn)檠h(huán)引用導(dǎo)致的內(nèi)存泄漏良蒸。
解決循環(huán)引用導(dǎo)致的內(nèi)存泄漏問題
從上面的例子里面可以看到,存在一種可能伍玖,ARC會(huì)維護(hù)一種永遠(yuǎn)不會(huì)置為0的實(shí)例:如果兩個(gè)實(shí)例互相持有對(duì)方的強(qiáng)引用嫩痰,那么會(huì)互相讓對(duì)方永遠(yuǎn)至少存在1的引用計(jì)數(shù),這就造成了循環(huán)強(qiáng)引用窍箍。
首先串纺,我們?cè)谄綍r(shí)的類關(guān)系設(shè)計(jì)的時(shí)候就會(huì)事先考慮好,盡量去避免對(duì)象實(shí)例之間的相互持有椰棘,也就避免了循環(huán)引用纺棺。
當(dāng)然,在設(shè)計(jì)上無法避免這樣的設(shè)定的時(shí)候邪狞,就可以對(duì)類關(guān)系之間的關(guān)系進(jìn)行重新定義祷蝌,把強(qiáng)引用改為弱引用或者無主引用。弱引用和無主引用允許發(fā)生了循環(huán)引用的兩個(gè)實(shí)例之間的一個(gè)實(shí)例引用另外一個(gè)實(shí)例而不保持強(qiáng)引用帆卓。
那么在什么時(shí)候用弱引用巨朦,什么時(shí)候用無主引用呢?
在兩個(gè)實(shí)例中鳞疲,假如一個(gè)實(shí)例引用的另外一個(gè)實(shí)例具有更短的生命周期罪郊,那么就使用弱引用(weak)來引用這個(gè)實(shí)例,如果引用的實(shí)例具有相同的或者更長的生命周期的時(shí)候尚洽,那么就使用無主引用(unowned)。
這兩個(gè)在使用的時(shí)候一定要注意靶累,假如你使用了無主引用引用了一個(gè)實(shí)例腺毫,你必須保證這個(gè)實(shí)例在引用者的生命周期內(nèi)不會(huì)被銷毀,如果被引用的這個(gè)實(shí)例卻先一步over了挣柬,你依然訪問這個(gè)無主引用潮酒,那么就會(huì)導(dǎo)致崩潰。弱引用不會(huì)對(duì)其引用的實(shí)例保持強(qiáng)引用邪蛔,也就不會(huì)去阻止ARC銷毀被引用的實(shí)例急黎。
那么,如何去保證無主引用的實(shí)例不會(huì)被銷毀呢侧到?一般來說勃教,引用的這個(gè)實(shí)例是永遠(yuǎn)存在的,不可能為nil匠抗。所以故源,我們可以這樣區(qū)分:兩個(gè)循環(huán)引用的實(shí)例所引用的屬性都允許為nil的時(shí)候,可以使用弱引用來解決汞贸;但是如果其中一個(gè)屬性的值不允許為nil的時(shí)候,即只要這個(gè)實(shí)例存在绳军,就一定會(huì)引用著另外一個(gè)實(shí)例的屬性印机,那么就可以使用無主引用來解決了。
假如出現(xiàn)了這種情況:兩個(gè)實(shí)例所引用的屬性都不允許為nil门驾,互相引用該怎么破射赛?假如有兩個(gè)類,一個(gè)是國家Country奶是,一個(gè)是城市City楣责。城市肯定屬于一個(gè)國家,一個(gè)國家也肯定會(huì)有一個(gè)首都城市诫隅。這就是互相引用了不為nil的屬性腐魂。
如下所示:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
我們可以看到在Country類里面有個(gè)capitalCity屬性,在City類里面有個(gè)country屬性逐纬。同時(shí)蛔屹,在County類的構(gòu)造器里面有對(duì)City的初始化賦值到自己的capitalCity屬性,在對(duì)City初始化的這個(gè)構(gòu)造器里面有一個(gè)country參數(shù)引用了Country實(shí)例(self)豁生。
構(gòu)造過程沒有進(jìn)行完的時(shí)候如何使用這個(gè)類的實(shí)例呢兔毒?這里使用了swift兩段式構(gòu)造的特性,第一段構(gòu)造甸箱,給每一個(gè)存儲(chǔ)型屬性指定一個(gè)初始值育叁;第二段構(gòu)造才會(huì)對(duì)屬性進(jìn)行進(jìn)一步定制。Country類里面的capitalCity類型設(shè)置為隱式解析可選類型芍殖,City豪嗽!
表示這個(gè)可選類型屬性初始化的值為nil,但是不需要進(jìn)行展開豌骏。因?yàn)橛谐跏贾祅il龟梦,所以順利度過第一段構(gòu)造,在name屬性也被構(gòu)造器賦值后窃躲,其實(shí)所有屬性就已經(jīng)初始化完成计贰,這個(gè)類已經(jīng)構(gòu)造完成,所以在第二段構(gòu)造過程中可以使用self
作為參數(shù)蒂窒,為capitalCity進(jìn)行重新賦值躁倒。
通過這種方式我們可以通過一條語句同時(shí)創(chuàng)建Country類和City類的實(shí)例,這樣就不會(huì)產(chǎn)生循環(huán)引用洒琢。在這里秧秉,我們一邊使用了無主引用,一邊使用了隱式解析纬凤,通過二段式構(gòu)造的特性巧妙解決了相互引用的問題福贞。
閉包中的循環(huán)引用
因?yàn)殚]包也是引用類型,所以閉包也和一個(gè)類一樣停士,如果一個(gè)類的某個(gè)屬性引用了閉包挖帘,而這個(gè)閉包中又引用了這個(gè)類實(shí)例完丽,那么就會(huì)出現(xiàn)閉包引起的循環(huán)引用問題。
swift很人性化的一點(diǎn)就是拇舀,假如你在閉包里面是用了這類實(shí)例的某個(gè)屬性或者某個(gè)方法逻族,就一定會(huì)提示你在前面加上self
,以提醒你注意循環(huán)引用被你一不小心就制造了出來骄崩。
swift維護(hù)有一個(gè)閉包捕獲列表聘鳞,列表的每一項(xiàng)都是由中括號(hào)括起來的一對(duì)值組成,第一個(gè)值是weak或者unowned要拂,另外一個(gè)值是對(duì)類實(shí)例的引用或者是初始化后的變量抠璃,比如[unowned self], [weak delegate = self.delegate]等。
當(dāng)閉包和它捕獲的實(shí)例相互引用并且是同時(shí)銷毀的時(shí)候脱惰,將閉包里面捕獲的引用定義為無主引用搏嗡。如果被捕獲的引用可能會(huì)變?yōu)閚il的時(shí)候,將它定義為弱引用拉一。