五、內(nèi)存管理

引用計(jì)數(shù)(Reference Count)是一個(gè)簡(jiǎn)單而有效的管理對(duì)象生命周期的方式。不管是Objective-C語(yǔ)言還是Swift語(yǔ)言,其內(nèi)存管理方式都是基于引用計(jì)數(shù)的。

1、引用計(jì)數(shù)

1.1 介紹

引用計(jì)數(shù)可以有效地管理對(duì)象生命周期:

  • 創(chuàng)建新對(duì)象時(shí),引用計(jì)數(shù)為1
  • 有一個(gè)新指針指向這個(gè)對(duì)象時(shí),引用計(jì)數(shù)加1
  • 某個(gè)指針不再指向這個(gè)對(duì)象時(shí),引用計(jì)數(shù)減1
  • 對(duì)象引用計(jì)數(shù)變?yōu)?時(shí),將對(duì)象銷(xiāo)毀,回收內(nèi)存
    由于引用計(jì)數(shù)簡(jiǎn)單有效,除了Objective-C語(yǔ)言外,微軟的COM(Component Object Model)、C++11(C++11提供了基于引用計(jì)數(shù)的智能指針share_prt)等語(yǔ)言也提供了基于引用計(jì)數(shù)的內(nèi)存管理方式。為了運(yùn)行示例,我們需要啟動(dòng)手工管理引用計(jì)數(shù)的模式,因?yàn)楝F(xiàn)在默認(rèn)的工程都開(kāi)啟了自動(dòng)的引用計(jì)數(shù),文件屬性加上-fno-objc-arc的編譯參數(shù):


    image.png

    image.png

    當(dāng)retainCount為0時(shí),對(duì)象會(huì)被銷(xiāo)毀,再向?qū)ο蟀l(fā)送消息,程序會(huì)報(bào)錯(cuò),因?yàn)檫@個(gè)時(shí)候指針指向的內(nèi)存已經(jīng)被銷(xiāo)毀了,成為了野指針。
    引用計(jì)數(shù)類(lèi)似于Linux文件系統(tǒng)里面的硬鏈接:

  • 用ln命令可以創(chuàng)建一個(gè)硬鏈接(類(lèi)似retain)
  • 刪除一個(gè)文件時(shí),系統(tǒng)會(huì)檢查文件的link count,大于1,不回收文件所占用的磁盤(pán)區(qū)域
  • 最后一次刪除,link count為1,系統(tǒng)才會(huì)執(zhí)行真正的刪除操作,把文件所占用的磁盤(pán)區(qū)域標(biāo)記成未使用

1.2 引用計(jì)數(shù)的作用

1.1中的示例,反映不出引用計(jì)數(shù)真正的用處,因?yàn)槭纠械膶?duì)象的生命周期只是在一個(gè)函數(shù)中,而在函數(shù)內(nèi)使用一個(gè)臨時(shí)的對(duì)象,通常用不上它的引用計(jì)數(shù),只要函數(shù)返回前將該對(duì)象銷(xiāo)毀即可。應(yīng)用計(jì)數(shù)真正排上用場(chǎng)的場(chǎng)景是在面向?qū)ο蟮某绦蛟O(shè)計(jì)架構(gòu)中:用于對(duì)象直接傳遞和共享數(shù)據(jù)。

image.png

假如對(duì)象A生成了一個(gè)屬性對(duì)象M,需要調(diào)用對(duì)象B的某一個(gè)方法,將對(duì)象M作為參數(shù)傳遞過(guò)去。對(duì)象M的釋放原則如下:

  • 對(duì)象A負(fù)責(zé)銷(xiāo)毀M,使用M的對(duì)象B可能只是臨時(shí)用一下對(duì)象M,也可能覺(jué)得對(duì)象M很重要,將它設(shè)置成自己的一個(gè)成員變量,那對(duì)象M的釋放時(shí)機(jī)就成了一個(gè)難題!
  • 暴力做法,對(duì)象A傳遞M后,立馬銷(xiāo)毀M,對(duì)象B需要復(fù)制一份M2,對(duì)象B自己管理M2的生命周期。但是,這樣會(huì)帶來(lái)更多的內(nèi)存申請(qǐng)、復(fù)制、釋放的工作。太影響性能。
  • 對(duì)象B負(fù)責(zé)銷(xiāo)毀M,對(duì)象B在用完后可以選擇銷(xiāo)毀M,這種做法強(qiáng)烈依賴于A、B兩個(gè)對(duì)象的配合,代碼維護(hù)者需要明確記住這種變成約定。對(duì)象M的申請(qǐng)和釋放代碼分散在不同的對(duì)象中,增加了管理的成本。如果遇到更復(fù)雜的情況,對(duì)象B又傳遞M給了對(duì)象C,實(shí)際的情況會(huì)更加復(fù)雜。

總結(jié):

引用計(jì)數(shù)可以很好的解決上述問(wèn)題,在參數(shù)M的傳遞過(guò)程中,哪些對(duì)象需要使用這個(gè)對(duì)象,就把它的引用計(jì)數(shù)+1,使用完了后引用計(jì)數(shù)-1。
所有對(duì)象都遵守這個(gè)規(guī)則的話,對(duì)象的生命周期管理就可以完全交給引用計(jì)數(shù)了。我們也可以很方便地享受到共享對(duì)象帶來(lái)的好處。

1.3 釋放對(duì)象

image.png
//引用計(jì)數(shù)為1時(shí),再次釋放
[object release]; 
//這時(shí)候引用計(jì)數(shù)為0,但是retainCount不會(huì)置0。在上圖中,程序異常崩潰了
NSLog(@"Reference Count = %lu", (unsigned long)[object retainCount]);

當(dāng)引用計(jì)數(shù)為0時(shí),對(duì)象的內(nèi)存會(huì)被回收,而我們向一個(gè)已經(jīng)被回收的對(duì)象發(fā)了一個(gè)retainCount消息,它的輸出結(jié)果應(yīng)該是不確定的,如果該對(duì)象的所占的內(nèi)存被復(fù)用了,那么就有可能造成程序異常崩潰。
當(dāng)最后一次執(zhí)行release,系統(tǒng)知道馬上就要回收內(nèi)存了,就沒(méi)有必要將retainCount減少1了,因?yàn)椴还軠p不減,該對(duì)象都肯定會(huì)被回收,它的所有的內(nèi)存區(qū)域,包括retainCount值也變得沒(méi)有意義。不將這個(gè)值從1變成0,可以減少一次內(nèi)存的操作,加速對(duì)象的回收。
例如Linux文件系統(tǒng)舉例,Linux文件系統(tǒng)下刪除一個(gè)文件,也不是真正地在文件的磁盤(pán)區(qū)域進(jìn)行抹除操作,而只是刪除該文件的索引節(jié)點(diǎn)號(hào)。與引用計(jì)數(shù)的內(nèi)存回收方式類(lèi)似,即回收時(shí)只做標(biāo)記,并不抹除相關(guān)的數(shù)據(jù)。

補(bǔ)充:

當(dāng)對(duì)象的引用計(jì)數(shù)變成0的時(shí)候,系統(tǒng)會(huì)調(diào)用delloc方法去銷(xiāo)毀對(duì)象。我們可以在delloc方法中去銷(xiāo)毀之前引用對(duì)象的指針,以及取消已經(jīng)訂閱的KVO,通知等。

1、調(diào)用 -release :引用計(jì)數(shù)變?yōu)榱?br> * 對(duì)象正在被銷(xiāo)毀,生命周期即將結(jié)束.
* 不能再有新的 __weak 弱引用, 否則將指向 nil.
* 調(diào)用 [self dealloc]
2、父類(lèi) 調(diào)用 -dealloc
* 繼承關(guān)系中最底層的父類(lèi) 在調(diào)用 -dealloc
* 如果是 MRC 代碼 則會(huì)手動(dòng)釋放實(shí)例變量們(iVars)
* 繼承關(guān)系中每一層的父類(lèi) 都在調(diào)用 -dealloc
3、NSObject 調(diào) -dealloc
* 只做一件事:調(diào)用 Objective-C runtime 中的 object_dispose() 方法
4、調(diào)用 object_dispose()
* 為 C++ 的實(shí)例變量們(iVars)調(diào)用 destructors
* 為 ARC 狀態(tài)下的 實(shí)例變量們(iVars) 調(diào)用 -release
* 解除所有使用 runtime Associate方法關(guān)聯(lián)的對(duì)象
* 解除所有 __weak 引用
* 調(diào)用 free()

1.4 循環(huán)引用及檢測(cè)

引用計(jì)數(shù)這種管理內(nèi)存的方式雖然很簡(jiǎn)單,但是有一個(gè)比較大的瑕疵,即它不能很好地解決循環(huán)引用問(wèn)題。


image.png

當(dāng)前對(duì)象A和對(duì)象B的引用計(jì)數(shù)都是1,如果想要將A和B銷(xiāo)毀,必須置引用計(jì)數(shù)為0,在沒(méi)有程序員介入的情況下,對(duì)象A要釋放,其引用計(jì)數(shù)必須減一,這樣其成員變量B的引用計(jì)數(shù)就會(huì)減一,但是對(duì)象A的引用計(jì)數(shù)要減一,必須讓對(duì)象B銷(xiāo)毀,即B的引用計(jì)數(shù)減一才可以,這樣對(duì)象A和B都依賴于對(duì)方,都在等待對(duì)方去釋放。

image.png

如上圖所示,多個(gè)對(duì)象間,依次引用了對(duì)方作為自己的成員變量,只有當(dāng)自己銷(xiāo)毀時(shí),才會(huì)將成員變量的引用計(jì)數(shù)減1,即依次持有,形成一個(gè)環(huán)狀,在真實(shí)的編程環(huán)境中,環(huán)越大就越難發(fā)現(xiàn)。

解決方案:

1、主動(dòng)斷開(kāi)
主動(dòng)斷開(kāi)循環(huán)引用依賴于程序員自己手工顯示地控制,相當(dāng)于回到“誰(shuí)申請(qǐng)誰(shuí)釋放”的手動(dòng)內(nèi)存管理,同時(shí)也依賴于程序員的能力,發(fā)現(xiàn)循環(huán)引用并在合適的時(shí)機(jī)斷開(kāi)循環(huán)引用回收內(nèi)存。
2、弱引用
弱引用,雖然持有對(duì)象,但并不增加引用計(jì)數(shù)。比如:delegate。

檢測(cè)循環(huán)引用

Xcode的Instruments工具集:
image.png

點(diǎn)擊Profile后,Xcode會(huì)打開(kāi)模擬器,并安裝程序到模擬器,但是不會(huì)運(yùn)行程序,然后在打開(kāi)Instruments工具集:
image.png

選擇Leaks,然后點(diǎn)擊Choose:
image.png
這個(gè)時(shí)候檢測(cè)面板會(huì)彈出,此時(shí)還沒(méi)有開(kāi)始檢測(cè),鼠標(biāo)懸停在左上角紅色按鈕上時(shí),顯示:Start an immediate mode recoding。點(diǎn)擊按鈕開(kāi)始檢測(cè)并記錄,這時(shí)程序會(huì)被顯示運(yùn)行:
image.png

上圖中,已經(jīng)檢測(cè)到了一次內(nèi)存泄漏,選中這次內(nèi)存泄漏,下面詳情區(qū)域切到Cycles & Roots:


image.png

使用Instrument顯示設(shè)備離線(device offline)解決方案:
關(guān)閉Instruments,然后Xcode依次進(jìn)行:清理(shift+command+K)+編譯(command+B)+運(yùn)行(command+I)

2、使用ARC

非ARC的應(yīng)用,遷移到ARC,有一些遷移成本,但是Xcode專(zhuān)門(mén)集成了遷移工具,成本已經(jīng)非常小了,而且,為了兼容第三方的非ARC開(kāi)源庫(kù),你也可以在工程中隨意使用編譯參數(shù)-fno-objc-arc,這個(gè)參數(shù)允許對(duì)部分文件關(guān)閉ARC:


image.png

雖然ARC是與IOS5一同推出的,但是由于ARC的實(shí)現(xiàn)機(jī)制是在編譯期完成的,所以使用ARC之后應(yīng)用仍然可以支持iOS4.3。稍微需要注意的是,如果要在ARC開(kāi)啟的情況下支持IOS4.3,需要將weak關(guān)鍵字換成__unsafe_unretained。

Core Foundation對(duì)象

    CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, "hello world", kCFStringEncodingUTF8);
//CF對(duì)象需要手動(dòng)管理
    CFRetain(str);
    CFRelease(str);

在ARC下,我們有時(shí)需要將一個(gè)Core Foundation對(duì)象轉(zhuǎn)換成一個(gè)OC對(duì)象:

  • ___bridge :只做類(lèi)型轉(zhuǎn)換,不修改相關(guān)對(duì)象的引用計(jì)數(shù),原來(lái)的Core Foundation在不用時(shí),需要調(diào)用CFRelease方法。
  • ___bridge_retained :類(lèi)型轉(zhuǎn)換后,將對(duì)象的引用計(jì)數(shù)加+1,原來(lái)的Core Foundation對(duì)象在不用時(shí),需要調(diào)用CFRelease方法。
  • ___bridge_transfer :類(lèi)型轉(zhuǎn)換后,將對(duì)象的引用計(jì)數(shù)交給ARC管理,原來(lái)的Core Foundation對(duì)象在不用時(shí),不需要調(diào)用CFRelease方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容