Objective-C高級(jí)編程之內(nèi)存管理篇

iOS的內(nèi)存管理是采用引用計(jì)數(shù)的方式,引用計(jì)數(shù)分為手動(dòng)引用計(jì)數(shù)和自動(dòng)引用計(jì)數(shù)(ARC)。前者要求開發(fā)者手動(dòng)管理內(nèi)存,自己負(fù)責(zé)內(nèi)存的申請與釋放,后者是蘋果推出的自動(dòng)管理內(nèi)存的方式,但其實(shí)質(zhì)只是編譯器幫助開發(fā)者做了內(nèi)存管理的工作。理解引用計(jì)數(shù)的內(nèi)存管理機(jī)制有助于我們寫出更加內(nèi)存安全的代碼。

內(nèi)存管理/引用計(jì)數(shù)

1. 引用計(jì)數(shù)的思考方式

引用計(jì)數(shù)的思考方式遵循以下四個(gè)原則:

  • 自己生成的對象,自己持有
    id obj = [NSObject alloc] init]
    alloc創(chuàng)建了一個(gè)對象(這里指內(nèi)存),init對此對象做初始化操作,obj持有了這個(gè)對象。同樣的,new/copy/mutablecopy也可用于生成并持有對象,除此四個(gè)關(guān)鍵字之外,以alloc/new/copy/mutablecopy開頭的方法也可用于生成并持有對象。
  • 非自己生成的對象,自己也能持有
    id obj = [NSMutableArray array]
    obj取得了array對象,但此時(shí)并不持有它。(事實(shí)上,array方法返回的是一個(gè)autorelease對象)
    [obj retain]
    通過retain方法,obj便持有了array對象 。
  • 不再需要自己持有的對象時(shí),釋放
    持有的對象不再需要時(shí),持有者應(yīng)當(dāng)負(fù)責(zé)釋放它。
    id obj = [NSObject alloc] init]
    [obj release]
    通過調(diào)用release方法可釋放obj持有的對象。
  • 非自己持有的對象,無法釋放
    id obj = [NSObject alloc] init]
    [obj release]
    [obj release] //程序?qū)⒈罎?/code>
    當(dāng)?shù)诙握{(diào)用release時(shí),由于obj已不再持有對象,程序就會(huì)發(fā)生異常。

2. 引用計(jì)數(shù)的實(shí)現(xiàn)

  • alloc/retain/release/dealloc
    alloc方法和retain方法會(huì)使對象的引用計(jì)數(shù)值加1,release方法使對象的引用計(jì)數(shù)值減1,當(dāng)對象的引用計(jì)數(shù)值為0時(shí),調(diào)用dealloc方法釋放對象。
    蘋果是用散列表來管理引用計(jì)數(shù)的,鍵值為對象內(nèi)存塊地址,對應(yīng)的值保存引用計(jì)數(shù)。圖示如下
鍵值為內(nèi)存塊地址哈希值的引用計(jì)數(shù)表

使用散列表來管理引用計(jì)數(shù)的好處是
1)散列表中存有內(nèi)存塊地址,可通過表中引用計(jì)數(shù)追溯到出問題的內(nèi)存塊地址(這在調(diào)試是很有幫助的)
2)對象內(nèi)存分配時(shí)無需再考慮引用計(jì)數(shù)所占用的內(nèi)存

  • autorelease
    autorelease是自動(dòng)釋放對象的方法,它是通過NSAutoreleasePool來實(shí)現(xiàn)的。它在對象超出自身的作用域之后,調(diào)用release方法去釋放對象,具體實(shí)現(xiàn)細(xì)節(jié)為:
    1)生成并持有NSAutoreleasePool對象
    2)調(diào)用autorelease方法,將對象添加到NSAutoreleasePool中
    3)待NSAutoreleasePool生命周期結(jié)束時(shí),所有添加到自動(dòng)釋放池中的對象均會(huì)被發(fā)送release消息來釋放自身
autorelease實(shí)現(xiàn)過程

代碼表示如下:

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];

cocoa框架中,使用NSRunloop來管理NSAutoreleasePool的生成、持有和釋放,runloop開始時(shí)會(huì)創(chuàng)建自動(dòng)釋放池,睡眠和退出時(shí)會(huì)銷毀自動(dòng)釋放池,圖解如下:


NSRunloop生成、持有,廢棄NSAutoreleasePool對象

很多時(shí)候,我們并不需要主動(dòng)使用NSAutoreleasePool來管理內(nèi)存,但是某些時(shí)候如果產(chǎn)生了大量的autorelease對象,而NSAutoreleasePool沒釋放前,這些對象便依舊存于內(nèi)存中,有可能會(huì)引發(fā)內(nèi)存不足的情況,此時(shí)我們可以考慮創(chuàng)建NSAutoreleasePool來及時(shí)釋放不需要的對象。當(dāng)我們創(chuàng)建了多個(gè)自動(dòng)釋放池時(shí),蘋果又是怎么管理它們的呢?答案是棧!

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool3 = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool3 drain];
[pool2 drain];
[pool drain];

很顯然,對象obj應(yīng)該會(huì)加入到pool3中,因?yàn)閜ool3是當(dāng)前正在使用的自動(dòng)釋放池。我們來看下蘋果底層相關(guān)的方法

class AutoreleasePoolPage 
{
...
public:
    static inline id autorelease(id obj) {}  //將一個(gè)對象添加到pool中

    static inline void *push() {}  //將新創(chuàng)建的pool壓入棧

    static inline void pop(void *token) {}  //將當(dāng)前的pool出棧
...
  }

ARC

1. 所有權(quán)修飾符

ARC下由編譯器幫助開發(fā)者自動(dòng)加入內(nèi)存管理代碼,因此編譯器必須知道對象何時(shí)在被持有,何時(shí)應(yīng)該被釋放,故蘋果引入了4個(gè)所有權(quán)修飾符:
__strong, __week, __unsafed_unretained, __autoreleasing

  • __strong修飾符
    __strong表示對對象的強(qiáng)引用,是id類型和對象類型默認(rèn)的所有權(quán)修飾符,以下兩行代碼實(shí)質(zhì)是一樣的
id obj = [NSObject alloc] init];
id __strong obj = [NSObject alloc] init];

帶有__strong修飾符的變量在超出其作用域時(shí),即變量被廢棄時(shí),其持有的對象也隨之被釋放,代碼角度看類似這樣

{
  id obj = [NSObject alloc] init];
  [obj release];
}

在超出大括號(hào)作用域后,obj被廢棄,其持有的對象也因強(qiáng)引用的失效而被釋放。

  • __week修飾符
    看起來__strong修飾符已經(jīng)能完美解決內(nèi)存管理的諸多問題,但事實(shí)上有一種情況是強(qiáng)引用無法解決的,即循環(huán)引用,而__week修飾符,這種弱引用方式,則可以完美解決循環(huán)引用的問題。
    我們先來看一下什么是循環(huán)引用。舉個(gè)例子,比如當(dāng)A持有了B的強(qiáng)引用,B也持有了A的強(qiáng)引用,由于A和B相互持有對象的強(qiáng)引用,導(dǎo)致A和B均無法被釋放,這便是出現(xiàn)了循環(huán)引用。
對象相互強(qiáng)引用

有時(shí)對象引用了自身,也會(huì)發(fā)生循環(huán)引用的現(xiàn)象。

自身強(qiáng)引用

循環(huán)引用的后果是會(huì)發(fā)生內(nèi)存泄漏(不再被需要的應(yīng)該廢棄的對象卻無法被釋放),那么__week是如何解決的呢?帶有__week修飾符的變量無法持有對象的實(shí)例,換句話說,強(qiáng)引用會(huì)使對象的引用計(jì)數(shù)增加,而弱引用不會(huì)。
id __week obj = [NSObject alloc] init];
上述代碼編譯器會(huì)產(chǎn)生警告,原因是對象被生成后,由于obj持有其弱引用,導(dǎo)致對象立即被釋放。帶有__week修飾符的變量在對象被釋放后自動(dòng)變成了nil,故上述代碼最終得到的obj是nil,將代碼改為如下即可消除警告。

id __strong obj0 = [NSObject alloc] init];
id __week obj = obj0;

obj0持有對象的強(qiáng)引用,所以對象不會(huì)被釋放,obj可以正確使用對象。當(dāng)obj0超出了作用域,強(qiáng)引用失效,對象被釋放,此時(shí)obj自動(dòng)變?yōu)閚il。如此便很容易明白,當(dāng)循環(huán)引用的兩個(gè)對象相互持有對方的弱引用時(shí)(或者其中一個(gè)持有的是弱引用),并不會(huì)影響到對象的釋放,也就不再會(huì)發(fā)生內(nèi)存泄露了。
和引用計(jì)數(shù)表類似,蘋果對于week變量的管理也是通過散列表來實(shí)現(xiàn)的。將賦值對象的地址作為鍵值,由于同一對象可能被多個(gè)week變量弱引用,故同一鍵值可能對應(yīng)一組week變量。由于__week修飾的變量會(huì)占用一定的CPU資源,因此除了解決循環(huán)引用的問題,盡量避免過多的使用week變量。

  • \ __unsafe_unretained修飾符
    __unsafe_unretained修飾符修飾的變量既不持有對象的強(qiáng)引用,也不持有對象的弱引用,它和__week修飾符一樣,實(shí)際上是獲得了一個(gè)指向?qū)ο蟮闹羔槪蚠_week不同的是,當(dāng)對象被釋放后,__week修飾的變量自動(dòng)變?yōu)閚il,__unsafe_unretained修飾的變量則成為了野指針!帶有__unsafe_unretained的變量不在編譯器內(nèi)存管理范圍內(nèi),編譯器是不對它做管理的,使用時(shí)應(yīng)當(dāng)謹(jǐn)慎確保其所指向的對象仍存在并未被釋放。
  • __autoreleasing修飾符
    ARC下的__autoreleasing相當(dāng)于調(diào)用對象的autorelease方法,并使用@autoreleasepool來替代非ARC下的NSAutoreleasePool功能。被__autoreleasing修飾的變量所持有的對象會(huì)被加入到自動(dòng)釋放池中。
ARC與非ARC下代碼對比

事實(shí)上,像__strong修飾符一樣,大多數(shù)時(shí)候,我們并不需要顯示對一個(gè)變量指定__autoreleasing修飾符。比如在取得非自己生成的對象引用時(shí)(使用除alloc/new/copy/mutablecopy以外的方法取得對象),變量被__week修飾符修飾時(shí),取得id或?qū)ο箢愋偷闹羔槙r(shí)(例如 id **obj),對象均會(huì)被自動(dòng)注冊到自動(dòng)釋放池中。

2. ARC規(guī)則

ARC有如下8個(gè)規(guī)則:

  • 不可使用retain/release/retainCount/dealloc方法
    由于ARC下編譯器會(huì)自動(dòng)在合適的位置幫助開發(fā)者插入內(nèi)存管理的代碼,因此不允許開發(fā)者再主動(dòng)調(diào)用內(nèi)存管理相關(guān)方法。
  • 不可使用NSAllocateObject/NSDeallocateObject方法
    事實(shí)上,alloc方法會(huì)調(diào)用NSAllocateObject來創(chuàng)建對象,并保存其引用計(jì)數(shù),單獨(dú)調(diào)用NSAllocateObject方法會(huì)對內(nèi)存管理造成混亂,因此禁止使用該方法自然也是合理的。
  • 不可顯示調(diào)用dealloc方法
    這里指的是開發(fā)者無需在dealloc方法中顯示調(diào)用[super dealloc],只需要做在釋放對象時(shí)一些必要的處理,比如移除之前注冊的某些觀察者。
  • 使用@autoreleasepool塊代替NSAutoreleasePool
    這個(gè)是顯而易見的,ARC下禁止使用NSAutoreleasePool,而采用@autoreleasepool塊代替。
  • 不能使用NSZone
    事實(shí)上,無論是手動(dòng)管理內(nèi)存還是使用ARC,NSZone(區(qū)域)在現(xiàn)在的運(yùn)行時(shí)系統(tǒng)中都是被忽略的。
  • 對象型變量不可作為C語言結(jié)構(gòu)體成員
    在C語言的規(guī)約下,結(jié)構(gòu)體成員的生命周期是無法管理的,而ARC下編譯器必須能夠正確的管理OC對象的生命周期,這顯然是矛盾的,故對象型變量不可作為C語言結(jié)構(gòu)體的成員。解決的方式有兩種,其一將對象型變量轉(zhuǎn)換為void *類型(指向不限定某一具體類型的指針),其二是用__unsafed_unretained來修飾變量,這相當(dāng)于告訴編譯器該對象不需要被編譯器管理。
  • 遵循內(nèi)存管理的方法命名規(guī)則
    一般而言,命名方法時(shí),謹(jǐn)慎使用以alloc/new/copy/mutablecopy開頭的方法名,這類方法應(yīng)當(dāng)返回給調(diào)用方應(yīng)該持有的對象。以init開頭的方法應(yīng)該返回實(shí)例對象。
  • 顯示轉(zhuǎn)換id和void
    在非ARC下,我們可以方便的直接對這兩種類型做轉(zhuǎn)換,如下所示
id obj = [NSObject alloc] init];
void * var = (void *)obj;
id obj2 = (id)var;

但是在ARC下,由于內(nèi)存管理交給了編譯器,因此編譯器需要明確每個(gè)對象的所有者,該對象是否還有所有者,當(dāng)無所有者時(shí),編譯器應(yīng)當(dāng)負(fù)責(zé)釋放該對象。因此我們在轉(zhuǎn)換類型時(shí)往往還需要考慮對象所有權(quán)問題。倘若只是想單純的賦值,那么我們可以使用__bridge修飾符來完成轉(zhuǎn)換。

id obj = [NSObject alloc] init];
void * var = (__bridge void *)obj;
id obj2 = (__bridge id)var;

與__bridge修飾符相關(guān)的兩個(gè)修飾符是__bridge_retained和__bridge_transfer,這兩個(gè)修飾符在完成轉(zhuǎn)換的同時(shí),還會(huì)對對象的所有權(quán)轉(zhuǎn)移做處理。
__bridge_retained修飾符修飾的變量在被賦值時(shí),還會(huì)獲得被賦值對象的所有權(quán),換句話說,會(huì)使對象引用計(jì)數(shù)增加。假定var是void *類型的變量,obj是id類型的變量,那么

var = (__bridge_retained void *)obj;

這等價(jià)于

var = (void *)obj;
[(id)var retain];

__bridge_transfer修飾符則正好和__bridge_retained相反,在賦值后被賦值對象隨即被釋放。同樣的,假定var是void *類型的變量,obj是id類型的變量,那么

obj = (__bridge_transfer id)var

這等價(jià)于

obj = (id)var;
[obj retain];
[(id)var release];

這種轉(zhuǎn)換常見于core Foundation對象與Foundation對象之間。前者是C語言類型對象,后者則是OC類型對象。

3. 屬性

ARC下,類屬性聲明和對應(yīng)的所有權(quán)修飾符是同樣的作用。如下所示

屬性聲明的屬性與所有權(quán)修飾符的對應(yīng)關(guān)系
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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