1. 引子
那些經(jīng)歷過手工管理內(nèi)存(MRC)時(shí)代的人們,一定對(duì) iOS 開發(fā)中的內(nèi)存管理記憶猶新锌雀。那個(gè)時(shí)候大約是 2010 年,國內(nèi) iOS 開發(fā)剛剛興起,那個(gè)時(shí)候的 iOS 開發(fā)過程是這樣的:
我們先寫好一段 iOS 的代碼劝评,開始運(yùn)行它,不出所料倦淀,它崩潰了蒋畜。在 MRC 時(shí)代,即使是最牛逼的 iOS 開發(fā)者撞叽,也不能保證一次性就寫出完美的內(nèi)存管理代碼姻成。于是,我們開始一步一步調(diào)試愿棋,試著打印出每個(gè)懷疑對(duì)象的引用計(jì)數(shù)(Retain Count)科展,然后,我們小心翼翼地插入合理的 retain
和release代碼糠雨。經(jīng)過一次又一次的應(yīng)用崩潰和調(diào)試才睹,終于有一次,應(yīng)用能夠正常運(yùn)行了甘邀!
是的琅攘,這就是那個(gè)年代的 iOS 開發(fā)者,通常情況下松邪,我們?cè)陂_發(fā)完一個(gè)功能后坞琴,需要再花好幾個(gè)小時(shí),才能把引用計(jì)數(shù)管理好测摔。
蘋果在 2011 年的時(shí)候置济,在 WWDC 大會(huì)上提出了自動(dòng)的引用計(jì)數(shù)(ARC)解恰。ARC 背后的原理是依賴編譯器的靜態(tài)分析能力,通過在編譯時(shí)找出合理的插入引用計(jì)數(shù)管理代碼浙于,從而徹底解放程序員护盈。
在 ARC 剛剛出來的時(shí)候,業(yè)界對(duì)此黑科技充滿了懷疑和觀望羞酗,加上現(xiàn)有的 MRC 代碼要做遷移本來也需要額外的成本腐宋,所以 ARC 并沒有被很快接受。直到 2013 年左右檀轨,蘋果認(rèn)為 ARC 技術(shù)足夠成熟胸竞,直接將 macOS(當(dāng)時(shí)叫 OS X)上的垃圾回收機(jī)制廢棄,從而使得 ARC 迅速被接受参萄。
2014 年的 WWDC 大會(huì)上卫枝,蘋果推出了 Swift 語言,而該語言仍然使用 ARC 技術(shù)讹挎,作為其內(nèi)存管理方式校赤。
為什么我要提這段歷史呢?就是因?yàn)楝F(xiàn)在的 iOS 開發(fā)者實(shí)在太舒服了筒溃,大部分時(shí)候马篮,他們根本都不用關(guān)心程序的內(nèi)存管理行為。但是怜奖,雖然 ARC 幫我們解決了引用計(jì)數(shù)的大部分問題浑测,一些年輕的 iOS 開發(fā)者仍然會(huì)做不好內(nèi)存管理工作。他們甚至不能理解常見的循環(huán)引用問題歪玲,而這些問題會(huì)導(dǎo)致內(nèi)存泄漏迁央,最終使得應(yīng)用運(yùn)行緩慢或者被系統(tǒng)終止進(jìn)程。
所以滥崩,我們每一個(gè) iOS 開發(fā)者漱贱,需要理解引用計(jì)數(shù)這種內(nèi)存管理方式,只有這樣夭委,才能處理好內(nèi)存管理相關(guān)的問題。
2. 什么是引用計(jì)數(shù)
引用計(jì)數(shù)(Reference Count)是一個(gè)簡單而有效的管理對(duì)象生命周期的方式募强。當(dāng)我們創(chuàng)建一個(gè)新對(duì)象的時(shí)候株灸,它的引用計(jì)數(shù)為 1,當(dāng)有一個(gè)新的指針指向這個(gè)對(duì)象時(shí)擎值,我們將其引用計(jì)數(shù)加 1慌烧,當(dāng)某個(gè)指針不再指向這個(gè)對(duì)象是,我們將其引用計(jì)數(shù)減 1鸠儿,當(dāng)對(duì)象的引用計(jì)數(shù)變?yōu)?0 時(shí)屹蚊,說明這個(gè)對(duì)象不再被任何指針指向了厕氨,這個(gè)時(shí)候我們就可以將對(duì)象銷毀,回收內(nèi)存汹粤。由于引用計(jì)數(shù)簡單有效命斧,除了 Objective-C 和 Swift 語言外,微軟的 COM(Component Object Model )嘱兼、C++11(C++11 提供了基于引用計(jì)數(shù)的智能指針 share_prt)等語言也提供了基于引用計(jì)數(shù)的內(nèi)存管理方式国葬。
為了更形象一些,我們?cè)賮砜匆欢?Objective-C 的代碼芹壕。新建一個(gè)工程汇四,因?yàn)楝F(xiàn)在默認(rèn)的工程都開啟了自動(dòng)的引用計(jì)數(shù) ARC(Automatic Reference Count),我們先修改工程設(shè)置踢涌,給 AppDelegate.m 加上 -fno-objc-arc
的編譯參數(shù)(如下圖所示)通孽,這個(gè)參數(shù)可以啟用手工管理引用計(jì)數(shù)的模式。
然后睁壁,我們?cè)谥休斎肴缦麓a背苦,可以通過 Log 看到相應(yīng)的引用計(jì)數(shù)的變化。
對(duì) Linux 文件系統(tǒng)比較了解的同學(xué)可能發(fā)現(xiàn)堡僻,引用計(jì)數(shù)的這種管理方式類似于文件系統(tǒng)里面的硬鏈接糠惫。在 Linux 文件系統(tǒng)中,我們用 ln
命令可以創(chuàng)建一個(gè)硬鏈接(相當(dāng)于我們這里的 retain)钉疫,當(dāng)刪除一個(gè)文件時(shí)(相當(dāng)于我們這里的 release)硼讽,系統(tǒng)調(diào)用會(huì)檢查文件的 link count 值,如果大于 1牲阁,則不會(huì)回收文件所占用的磁盤區(qū)域固阁。直到最后一次刪除前,系統(tǒng)發(fā)現(xiàn) link count 值為 1城菊,則系統(tǒng)才會(huì)執(zhí)行直正的刪除操作备燃,把文件所占用的磁盤區(qū)域標(biāo)記成未用。
3. 我們?yōu)槭裁葱枰糜?jì)數(shù)
從上面那個(gè)簡單的例子中凌唬,我們還看不出來引用計(jì)數(shù)真正的用處并齐。因?yàn)樵搶?duì)象的生命期只是在一個(gè)函數(shù)內(nèi),所以在真實(shí)的應(yīng)用場景下客税,我們?cè)诤瘮?shù)內(nèi)使用一個(gè)臨時(shí)的對(duì)象况褪,通常是不需要修改它的引用計(jì)數(shù)的,只需要在函數(shù)返回前將該對(duì)象銷毀即可更耻。
引用計(jì)數(shù)真正派上用場的場景是在面向?qū)ο蟮某绦蛟O(shè)計(jì)架構(gòu)中测垛,用于對(duì)象之間傳遞和共享數(shù)據(jù)。我們舉一個(gè)具體的例子:
假如對(duì)象 A 生成了一個(gè)對(duì)象 M秧均,需要調(diào)用對(duì)象 B 的某一個(gè)方法食侮,將對(duì)象 M 作為參數(shù)傳遞過去号涯。在沒有引用計(jì)數(shù)的情況下,一般內(nèi)存管理的原則是 “誰申請(qǐng)誰釋放”锯七,那么對(duì)象 A 就需要在對(duì)象 B 不再需要對(duì)象 M 的時(shí)候链快,將對(duì)象 M 銷毀。但對(duì)象 B 可能只是臨時(shí)用一下對(duì)象 M起胰,也可能覺得對(duì)象 M 很重要久又,將它設(shè)置成自己的一個(gè)成員變量,那這種情況下效五,什么時(shí)候銷毀對(duì)象 M 就成了一個(gè)難題地消。
對(duì)于這種情況,有一個(gè)暴力的做法畏妖,就是對(duì)象 A 在調(diào)用完對(duì)象 B 之后脉执,馬上就銷毀參數(shù)對(duì)象 M,然后對(duì)象 B 需要將參數(shù)另外復(fù)制一份戒劫,生成另一個(gè)對(duì)象 M2半夷,然后自己管理對(duì)象 M2 的生命期。但是這種做法有一個(gè)很大的問題迅细,就是它帶來了更多的內(nèi)存申請(qǐng)巫橄、復(fù)制、釋放的工作茵典。本來一個(gè)可以復(fù)用的對(duì)象湘换,因?yàn)椴环奖愎芾硭纳冢秃唵蔚陌阉N毀统阿,又重新構(gòu)造一份一樣的彩倚,實(shí)在太影響性能。如下圖所示:
我們另外還有一種辦法扶平,就是對(duì)象 A 在構(gòu)造完對(duì)象 M 之后帆离,始終不銷毀對(duì)象 M,由對(duì)象 B 來完成對(duì)象 M 的銷毀工作结澄。如果對(duì)象 B 需要長時(shí)間使用對(duì)象 M哥谷,它就不銷毀它,如果只是臨時(shí)用一下麻献,則可以用完后馬上銷毀呼巷。這種做法看似很好地解決了對(duì)象復(fù)制的問題,但是它強(qiáng)烈依賴于 AB 兩個(gè)對(duì)象的配合赎瑰,代碼維護(hù)者需要明確地記住這種編程約定。而且破镰,由于對(duì)象 M 的申請(qǐng)是在對(duì)象 A 中餐曼,釋放在對(duì)象 B 中压储,使得它的內(nèi)存管理代碼分散在不同對(duì)象中,管理起來也非常費(fèi)勁源譬。如果這個(gè)時(shí)候情況再復(fù)雜一些,例如對(duì)象 B 需要再向?qū)ο?C 傳遞對(duì)象 M,那么這個(gè)對(duì)象在對(duì)象 C 中又不能讓對(duì)象 C 管理搞坝。所以這種方式帶來的復(fù)雜性更大钞护,更不可取。
所以引用計(jì)數(shù)很好的解決了這個(gè)問題养渴,在參數(shù) M 的傳遞過程中雷绢,哪些對(duì)象需要長時(shí)間使用這個(gè)對(duì)象,就把它的引用計(jì)數(shù)加 1理卑,使用完了之后再把引用計(jì)數(shù)減 1翘紊。所有對(duì)象都遵守這個(gè)規(guī)則的話,對(duì)象的生命期管理就可以完全交給引用計(jì)數(shù)了藐唠。我們也可以很方便地享受到共享對(duì)象帶來的好處帆疟。
4. 不要向已經(jīng)釋放的對(duì)象發(fā)送消息
有些同學(xué)想測試當(dāng)對(duì)象釋放時(shí),其 retainCount
是否變成了 0宇立,他們的試驗(yàn)代碼如下:
我們注意到踪宠,最后一次輸出,引用計(jì)數(shù)并沒有變成 0妈嘹。這是為什么呢柳琢?因?yàn)樵搶?duì)象的內(nèi)存已經(jīng)被回收,而我們向一個(gè)已經(jīng)被回收的對(duì)象發(fā)了一個(gè) retainCount 消息蟋滴,所以它的輸出結(jié)果應(yīng)該是不確定的染厅,如果該對(duì)象所占的內(nèi)存被復(fù)用了,那么就有可能造成程序異常崩潰津函。
那為什么在這個(gè)對(duì)象被回收之后肖粮,這個(gè)不確定的值是 1 而不是 0 呢?這是因?yàn)楫?dāng)最后一次執(zhí)行 release 時(shí)尔苦,系統(tǒng)知道馬上就要回收內(nèi)存了涩馆,就沒有必要再將 retainCount 減 1 了,因?yàn)椴还軠p不減 1允坚,該對(duì)象都肯定會(huì)被回收魂那,而對(duì)象被回收后,它的所有的內(nèi)存區(qū)域稠项,包括 retainCount 值也變得沒有意義涯雅。不將這個(gè)值從 1 變成 0,可以減少一次內(nèi)存的寫操作展运,加速對(duì)象的回收活逆。
拿我們之前提到的 Linux 文件系統(tǒng)舉列精刷,Linux 文件系統(tǒng)下刪除一個(gè)文件,也不是真正的將文件的磁盤區(qū)域進(jìn)行抹除操作蔗候,而只是刪除該文件的索引節(jié)點(diǎn)號(hào)怒允。這也和引用計(jì)數(shù)的內(nèi)存回收方式類似,即回收時(shí)只做標(biāo)記锈遥,并不抹除相關(guān)的數(shù)據(jù)纫事。
5. ARC 下的內(nèi)存管理問題
ARC 能夠解決 iOS 開發(fā)中 90% 的內(nèi)存管理問題,但是另外還有 10% 內(nèi)存管理所灸,是需要開發(fā)者自己處理的丽惶,這主要就是與底層 Core Foundation 對(duì)象交互的那部分,底層的 Core Foundation 對(duì)象由于不在 ARC 的管理下庆寺,所以需要自己維護(hù)這些對(duì)象的引用計(jì)數(shù)蚊夫。
對(duì)于 ARC 盲目依賴的 iOS 新人們,由于不知道引用計(jì)數(shù)懦尝,他們的問題主要體現(xiàn)在:
過度使用 block 之后知纷,無法解決循環(huán)引用問題。
遇到底層 Core Foundation 對(duì)象陵霉,需要自己手工管理它們的引用計(jì)數(shù)時(shí)琅轧,顯得一籌莫展。
5.1 循環(huán)引用(Reference Cycle)問題
引用計(jì)數(shù)這種管理內(nèi)存的方式雖然很簡單踊挠,但是有一個(gè)比較大的瑕疵乍桂,即它不能很好的解決循環(huán)引用問題。如下圖所示:對(duì)象 A 和對(duì)象 B效床,相互引用了對(duì)方作為自己的成員變量睹酌,只有當(dāng)自己銷毀時(shí),才會(huì)將成員變量的引用計(jì)數(shù)減 1剩檀。因?yàn)閷?duì)象 A 的銷毀依賴于對(duì)象 B 銷毀憋沿,而對(duì)象 B 的銷毀與依賴于對(duì)象 A 的銷毀,這樣就造成了我們稱之為循環(huán)引用(Reference Cycle)的問題沪猴,這兩個(gè)對(duì)象即使在外界已經(jīng)沒有任何指針能夠訪問到它們了辐啄,它們也無法被釋放。
不止兩對(duì)象存在循環(huán)引用問題运嗜,多個(gè)對(duì)象依次持有對(duì)方壶辜,形式一個(gè)環(huán)狀,也可以造成循環(huán)引用問題担租,而且在真實(shí)編程環(huán)境中砸民,環(huán)越大就越難被發(fā)現(xiàn)。下圖是 4 個(gè)對(duì)象形成的循環(huán)引用問題。
5.2 主動(dòng)斷開循環(huán)引用
解決循環(huán)引用問題主要有兩個(gè)辦法阱洪,第一個(gè)辦法是我明確知道這里會(huì)存在循環(huán)引用便贵,在合理的位置主動(dòng)斷開環(huán)中的一個(gè)引用,使得對(duì)象得以回收冗荸。如下圖所示:
主動(dòng)斷開循環(huán)引用這種方式常見于各種與 block 相關(guān)的代碼邏輯中。例如在我開源的 YTKNetwork 網(wǎng)絡(luò)庫中利耍,網(wǎng)絡(luò)請(qǐng)求的回調(diào) block 是被持有的蚌本,但是如果這個(gè) block 中又存在對(duì)于 View Controller 的引用,就很容易產(chǎn)生從循環(huán)引用隘梨,因?yàn)椋?br>
Controller 持有了網(wǎng)絡(luò)請(qǐng)求對(duì)象
網(wǎng)絡(luò)請(qǐng)求對(duì)象持有了回調(diào)的 block
回調(diào)的 block 里面使用了 self
程癌,所以持有了 Controller
解決辦法就是,在網(wǎng)絡(luò)請(qǐng)求結(jié)束后轴猎,網(wǎng)絡(luò)請(qǐng)求對(duì)象執(zhí)行完 block 之后嵌莉,主動(dòng)釋放對(duì)于 block 的持有,以便打破循環(huán)引用捻脖。相關(guān)的代碼見:
不過锐峭,主動(dòng)斷開循環(huán)引用這種操作依賴于程序員自己手工顯式地控制,相當(dāng)于回到了以前 “誰申請(qǐng)誰釋放” 的內(nèi)存管理年代可婶,它依賴于程序員自己有能力發(fā)現(xiàn)循環(huán)引用并且知道在什么時(shí)機(jī)斷開循環(huán)引用回收內(nèi)存(這通常與具體的業(yè)務(wù)邏輯相關(guān))沿癞,所以這種解決方法并不常用,更常見的辦法是使用弱引用 (weak reference) 的辦法矛渴。
5.3 使用弱引用
弱引用雖然持有對(duì)象椎扬,但是并不增加引用計(jì)數(shù),這樣就避免了循環(huán)引用的產(chǎn)生具温。在 iOS 開發(fā)中蚕涤,弱引用通常在 delegate 模式中使用。舉個(gè)例子來說铣猩,兩個(gè) ViewController A 和 B揖铜,ViewController A 需要彈出 ViewController B,讓用戶輸入一些內(nèi)容剂习,當(dāng)用戶輸入完成后蛮位,ViewController B 需要將內(nèi)容返回給 ViewController A。這個(gè)時(shí)候鳞绕,View Controller 的 delegate 成員變量通常是一個(gè)弱引用失仁,以避免兩個(gè) ViewController 相互引用對(duì)方造成循環(huán)引用問題,如下所示:
5.4 弱引用的實(shí)現(xiàn)原理
弱引用的實(shí)現(xiàn)原理是這樣们何,系統(tǒng)對(duì)于每一個(gè)有弱引用的對(duì)象萄焦,都維護(hù)一個(gè)表來記錄它所有的弱引用的指針地址。這樣,當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為 0 時(shí)拂封,系統(tǒng)就通過這張表茬射,找到所有的弱引用指針,繼而把它們都置成 nil冒签。
從這個(gè)原理中在抛,我們可以看出,弱引用的使用是有額外的開銷的萧恕。雖然這個(gè)開銷很小刚梭,但是如果一個(gè)地方我們肯定它不需要弱引用的特性,就不應(yīng)該盲目使用弱引用票唆。舉個(gè)例子朴读,有人喜歡在手寫界面的時(shí)候,將所有界面元素都設(shè)置成 weak 的走趋,這某種程度上與 Xcode 通過 Storyboard 拖拽生成的新變量是一致的衅金。但是我個(gè)人認(rèn)為這樣做并不太合適。因?yàn)椋?br>
我們?cè)趧?chuàng)建這個(gè)對(duì)象時(shí)簿煌,需要注意臨時(shí)使用一個(gè)強(qiáng)引用持有它氮唯,否則因?yàn)?weak 變量并不持有對(duì)象,就會(huì)造成一個(gè)對(duì)象剛被創(chuàng)建就銷毀掉啦吧。
大部分 ViewController 的視圖對(duì)象的生命周期與 ViewController 本身是一致的您觉,沒有必要額外做這個(gè)事情。
早先蘋果這么設(shè)計(jì)授滓,是有歷史原因的琳水。在早年,當(dāng)時(shí)系統(tǒng)收到 Memory Warning 的時(shí)候般堆,ViewController 的 View 會(huì)被 unLoad 掉在孝。這個(gè)時(shí)候,使用 weak 的視圖變量是有用的淮摔,可以保持這些內(nèi)存被回收私沮。但是這個(gè)設(shè)計(jì)已經(jīng)被廢棄了,替代方案是將相關(guān)視圖的 CALayer 對(duì)應(yīng)的 CABackingStore 類型的內(nèi)存區(qū)會(huì)被標(biāo)記成 volatile 類型和橙,詳見《再見仔燕,viewDidUnload方法》。
5.5 使用 Xcode 檢測循環(huán)引用
Xcode 的 Instruments 工具集可以很方便的檢測循環(huán)引用魔招。為了測試效果晰搀,我們?cè)谝粋€(gè)測試用的 ViewController 中填入以下代碼,該代碼中的 firstArray和 secondArray相互引用了對(duì)方办斑,構(gòu)成了循環(huán)引用外恕。
在 Xcode 的菜單欄選擇:Product -> Profile杆逗,然后選擇 “Leaks”,再點(diǎn)擊右下角的”Profile” 按鈕開始檢測鳞疲。如下圖
這個(gè)時(shí)候 iOS 模擬器會(huì)運(yùn)行起來罪郊,我們?cè)谀M器里進(jìn)行一些界面的切換操作。稍等幾秒鐘尚洽,就可以看到 Instruments 檢測到了我們的這次循環(huán)引用悔橄。Instruments 中會(huì)用一條紅色的條來表示一次內(nèi)存泄漏的產(chǎn)生。如下圖所示:
我們可以切換到 Leaks 這欄腺毫,點(diǎn)擊”Cycles & Roots”橄维,就可以看到以圖形方式顯示出來的循環(huán)引用。這樣我們就可以非常方便地找到循環(huán)引用的對(duì)象了拴曲。
5.6 Core Foundation 對(duì)象的內(nèi)存管理
下面我們就來簡單介紹一下對(duì)底層 Core Foundation 對(duì)象的內(nèi)存管理。底層的 Core Foundation 對(duì)象凛忿,在創(chuàng)建時(shí)大多以 XxxCreateWithXxx 這樣的方式創(chuàng)建澈灼,例如:
對(duì)于 CFRetain和 CFRelease兩個(gè)方法,讀者可以直觀地認(rèn)為店溢,這與 Objective-C 對(duì)象的 retain和 release方法等價(jià)叁熔。
所以對(duì)于底層 Core Foundation 對(duì)象,我們只需要延續(xù)以前手工管理引用計(jì)數(shù)的辦法即可床牧。
除此之外荣回,還有另外一個(gè)問題需要解決。在 ARC 下戈咳,我們有時(shí)需要將一個(gè) Core Foundation 對(duì)象轉(zhuǎn)換成一個(gè) Objective-C 對(duì)象心软,這個(gè)時(shí)候我們需要告訴編譯器,轉(zhuǎn)換過程中的引用計(jì)數(shù)需要做如何的調(diào)整著蛙。這就引入了bridge
相關(guān)的關(guān)鍵字删铃,以下是這些關(guān)鍵字的說明:
- __bridge: 只做類型轉(zhuǎn)換,不修改相關(guān)對(duì)象的引用計(jì)數(shù)踏堡,原來的 Core Foundation 對(duì)象在不用時(shí)猎唁,需要調(diào)用 CFRelease 方法。
- __bridge_retained:類型轉(zhuǎn)換后顷蟆,將相關(guān)對(duì)象的引用計(jì)數(shù)加 1诫隅,原來的 Core Foundation 對(duì)象在不用時(shí),需要調(diào)用CFRelease 方法帐偎。
- __bridge_transfer:類型轉(zhuǎn)換后逐纬,將該對(duì)象的引用計(jì)數(shù)交給 ARC 管理,Core Foundation 對(duì)象在不用時(shí)肮街,不再需要調(diào)用 CFRelease 方法风题。
我們根據(jù)具體的業(yè)務(wù)邏輯,合理使用上面的 3 種轉(zhuǎn)換關(guān)鍵字,就可以解決 Core Foundation 對(duì)象與 Objective-C 對(duì)象相對(duì)轉(zhuǎn)換的問題了沛硅。
6 總結(jié)
**1. **在 ARC 的幫助下眼刃,iOS 開發(fā)者的內(nèi)存管理工作已經(jīng)被大大減輕,但是我們?nèi)匀恍枰斫庖糜?jì)數(shù)這種內(nèi)存管理方式的優(yōu)點(diǎn)和常見問題摇肌,特別要注意解決循環(huán)引用問題擂红。對(duì)于循環(huán)引用問題有兩種主要的解決辦法,一是主動(dòng)斷開循環(huán)引用围小,二是使用弱引用的方式避免循環(huán)引用昵骤。對(duì)于 Core Foundation 對(duì)象,由于不在 ARC 管理之下肯适,我們?nèi)匀恍枰永m(xù)以前手工管理引用計(jì)數(shù)的辦法变秦。
**2. **在調(diào)試內(nèi)存問題時(shí),Instruments 工具可以很好地對(duì)我們進(jìn)行輔助框舔,善用 Instruments 可以節(jié)省我們大量的調(diào)試時(shí)間蹦玫。