淺明分析Swift循環(huán)引用

看過不少分析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í)候,將它定義為弱引用拉一。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末采盒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蔚润,更是在濱河造成了極大的恐慌磅氨,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫡纠,死亡現(xiàn)場離奇詭異烦租,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)除盏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門左权,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痴颊,你說我怎么就攤上這事÷藕兀” “怎么了蠢棱?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甩栈。 經(jīng)常有香客問我泻仙,道長,這世上最難降的妖魔是什么量没? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任玉转,我火速辦了婚禮,結(jié)果婚禮上殴蹄,老公的妹妹穿的比我還像新娘究抓。我一直安慰自己猾担,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布刺下。 她就那樣靜靜地躺著绑嘹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪橘茉。 梳的紋絲不亂的頭發(fā)上工腋,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音畅卓,去河邊找鬼擅腰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛翁潘,可吹牛的內(nèi)容都是我干的趁冈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼唐础,長吁一口氣:“原來是場噩夢啊……” “哼箱歧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起一膨,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤呀邢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后豹绪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體价淌,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年瞒津,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蝉衣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巷蚪,死狀恐怖病毡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屁柏,我是刑警寧澤啦膜,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站淌喻,受9級(jí)特大地震影響僧家,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜裸删,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一八拱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦肌稻、人聲如沸清蚀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轧铁。三九已至,卻和暖如春旦棉,著一層夾襖步出監(jiān)牢的瞬間齿风,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工绑洛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留救斑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓真屯,卻偏偏與公主長得像脸候,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绑蔫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容