ARC環(huán)境下iOS內(nèi)存管理總結(jié)

自動(dòng)引用計(jì)數(shù),又稱ARC(Automatic Reference Counting)是蘋果在iOS5中引入的重要特性,它減少了我們?cè)趦?nèi)存管理時(shí)的麻煩,讓我們可以把更多的精力放在其它更重要的事情上。

雖然ARC給我們帶來了很多方便,但如果開發(fā)者不了解基本的內(nèi)存管理知識(shí),還是會(huì)在開發(fā)工作中遇到很多問題。所以,我總結(jié)了ARC環(huán)境下應(yīng)該知道的內(nèi)存管理知識(shí),供諸位參考。


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

要了解ARC,必須先了解Objective-C中對(duì)象的內(nèi)存管理機(jī)制以及手動(dòng)管理引用計(jì)數(shù)(MRR,Manual Retain-Release)。

Objective-C內(nèi)存管理機(jī)制

Objective-C中的對(duì)象都是基于引用計(jì)數(shù)來管理生命周期的。簡(jiǎn)單來說就是,我們?cè)谛枰钟幸粋€(gè)對(duì)象時(shí),調(diào)用retain讓它的引用計(jì)數(shù)+1。不需要這個(gè)對(duì)象的時(shí)候,調(diào)用release讓它的引用計(jì)數(shù)-1。當(dāng)一個(gè)對(duì)象引用計(jì)數(shù)為0的時(shí)候,這個(gè)對(duì)象就會(huì)被自動(dòng)銷毀。

MRR

我們?cè)谑謩?dòng)管理引用計(jì)數(shù)的時(shí)候,要明確地控制對(duì)象的生命周期,顯式的調(diào)用每一個(gè)retain和release。我們必須清楚的了解每個(gè)接口對(duì)引用計(jì)數(shù)的處理(如把一個(gè)對(duì)象放到數(shù)組里引用計(jì)數(shù)會(huì)被+1,用alloc創(chuàng)建的對(duì)象的引用計(jì)數(shù)一開始就是1,用哪些接口創(chuàng)建的對(duì)象是已經(jīng)被調(diào)用過autorelease的等等)。在處理引用計(jì)數(shù)時(shí)稍有疏忽,就可能導(dǎo)致程序崩潰或內(nèi)存泄漏。

ARC

ARC是編譯器通過對(duì)代碼的靜態(tài)分析,確定對(duì)象的生命周期,并在合適的位置自動(dòng)加上retain和release的機(jī)制。把內(nèi)存管理交給編譯器以后,我們不需要再調(diào)用任何的retain和release了。ARC減少了MRR帶來的思考負(fù)擔(dān),減少了內(nèi)存問題出現(xiàn)的可能性,也大幅減少了代碼量。

下圖是蘋果的文檔中對(duì)ARC效果的描述,略顯夸張的展示出了ARC的好處。



ARC的內(nèi)存管理

ARC簡(jiǎn)化了引用計(jì)數(shù)的概念,它把變量對(duì)對(duì)象的引用分為強(qiáng)引用和弱引用兩種。所有ARC下的內(nèi)存管理原則都將基于這兩個(gè)概念。
強(qiáng)引用表示變量擁有對(duì)象的所有權(quán)。多個(gè)變量有可能同時(shí)持有同一個(gè)對(duì)象的強(qiáng)引用。當(dāng)一個(gè)對(duì)象沒有被任何變量強(qiáng)引用,它就會(huì)被釋放。
弱引用表示引用但不擁有對(duì)象,它可以避免兩個(gè)對(duì)象相互強(qiáng)引用導(dǎo)致的內(nèi)存泄漏問題。

所有權(quán)修飾符

ARC下的變量都會(huì)被加上下面幾種所有權(quán)修飾符,它們指定了變量對(duì)其指向?qū)ο蟮乃袡?quán)形式。整個(gè)ARC的規(guī)則也正是圍繞著這幾個(gè)修飾符運(yùn)作:
__strong:表示對(duì)對(duì)象的強(qiáng)引用。它是變量修飾符的默認(rèn)值,也就是說只要沒有顯式的給變量加上所有權(quán)修飾符,它就是__strong的。__strong變量在離開作用域范圍后會(huì)被廢棄(其實(shí)就是編譯器插入了一個(gè)release),對(duì)對(duì)象的強(qiáng)引用也隨之消失。
__weak:表示弱引用。當(dāng)其指向的對(duì)象被釋放時(shí),這個(gè)變量會(huì)被置成nil。
__autoreleasing:表示將修飾的對(duì)象加入autoreleasepool中,在autoreleasepool被銷毀時(shí)自動(dòng)釋放對(duì)對(duì)象的強(qiáng)引用。在@autoreleasepool的代碼塊中的變量都會(huì)被加上這個(gè)修飾符,在超出代碼塊范圍后釋放。詳見下面autorelease的部分。
__unsafe_unretained:表示既不持有對(duì)象的強(qiáng)引用,也不持有弱引用(對(duì)象析構(gòu)時(shí)它不會(huì)被置為nil)。正如它的名字描述,它是不安全的。它只是在iOS5之前用來代替__weak。

屬性修飾符

在使用MRR的時(shí)候,我們可以給property添加retain、assign、copy這幾種修飾符,來設(shè)置想要的內(nèi)存管理模式,用下面這段代碼來說明這三種修飾符的作用:

// MRR環(huán)境
@property (nonatomic, retain) NSObject* retainedObject;
@property (nonatomic, assign) NSObject* assignedObject;
@property (nonatomic, copy) NSMutableString* copiedObject;

- (void)testProperties {
  NSObject* objectA = [[NSObject alloc] init]; // objectA引用計(jì)數(shù)為1。
  self.retainedObject = objectA; // self.retainedObject和objectA指向同一個(gè)對(duì)象,且該對(duì)象引用計(jì)數(shù)為2。
 
  NSObject* objectB = [[NSObject alloc] init]; // objectB引用計(jì)數(shù)為1。
  self.assignedObject = objectB; // self.assignedObject和objectB指向同一個(gè)對(duì)象,且該對(duì)象引用計(jì)數(shù)為1。
    
  NSMutableString* objectC = [[NSMutableString alloc] initWithFormat:@"test"]; // objectC引用計(jì)數(shù)為1    
  self.copiedObject = objectC; // self.copiedObject和objectC指向兩個(gè)不同的對(duì)象,兩個(gè)對(duì)象引用計(jì)數(shù)都為1。

  // 這里可能會(huì)有疑問,為什么copy的例子用的是NSMutableString而不是NSObject了或者NSString?
  // 因?yàn)镹SObject沒有實(shí)現(xiàn)NSCopying協(xié)議,沒法復(fù)制。
  // 而使用NSString會(huì)導(dǎo)致self.copiedObject和objectC因?yàn)榫幾g器優(yōu)化而指向相同的對(duì)象。
    
  // 因?yàn)槭荕RR環(huán)境,我們要釋放我們自己分配的內(nèi)存,否則會(huì)產(chǎn)生內(nèi)存泄漏。
  [objectA release];
  [objectC release];
  // 當(dāng)然,給分配內(nèi)存的代碼加上autorelease也行:
  // NSObject* objectA = [[[NSObject alloc] init] autorelease];
  // NSMutableString* objectC = [[[NSMutableString alloc] init] autorelease];
}

- (void)dealloc {
  // MRR要在析構(gòu)時(shí)手動(dòng)清理內(nèi)存。
  [_objectA release];
  [_objectB release];
  [_objectC release];

  [super dealloc];
}

在ARC環(huán)境下,增加了兩種新的修飾符:strong和weak,分別對(duì)應(yīng)強(qiáng)引用和弱引用:

// ARC環(huán)境
@property (nonatomic, strong) NSObject* strongObject;
@property (nonatomic, weak) NSObject* weakObject;

- (void)testProperties {
  NSObject* objectD = [[NSObject alloc] init]; // objectD持有對(duì)象D的強(qiáng)引用。
  self.strongObject = objectD; // self.strongObject和objectD都持有對(duì)象D的強(qiáng)引用

  NSObject* objectE = [[NSObject alloc] init]; // objectE持有對(duì)象E的強(qiáng)引用。
  self.weakObject = objectE; // self.weakObject持有對(duì)象E的弱引用

  // 在ARC環(huán)境下,這個(gè)方法執(zhí)行完后,objectD和objectE對(duì)對(duì)象D和E的強(qiáng)引用會(huì)消失。
  // 這時(shí)候self.strongObject仍然持有對(duì)對(duì)象D的強(qiáng)引用。
  // self.weakObject之前對(duì)對(duì)象E持有的是弱引用,對(duì)象E析構(gòu)。self.weakObject的指針被置為nil。
}

// 與MRR不同,ARC環(huán)境下,self對(duì)象析構(gòu)時(shí),self.strongObject對(duì)對(duì)象E的強(qiáng)引用自動(dòng)消失,對(duì)象E自動(dòng)析構(gòu)。

Retain Cycle(保留環(huán))

ARC確實(shí)幫助我們避免了許多內(nèi)存管理的問題。但在ARC環(huán)境下,有一類問題還是需要被妥善的處理,這類問題叫做Retain Cycle(保留環(huán))。

ARC環(huán)境下的對(duì)象在沒有被強(qiáng)引用時(shí)就會(huì)被釋放,當(dāng)兩個(gè)對(duì)象互相對(duì)對(duì)方持有強(qiáng)引用時(shí),這兩個(gè)對(duì)象就永遠(yuǎn)不會(huì)被釋放了。這就導(dǎo)致了上面說的保留環(huán)問題。

要解決保留環(huán)的思路也簡(jiǎn)單,就是理清這兩個(gè)對(duì)象之間的所有權(quán)關(guān)系,再讓其中一個(gè)對(duì)象對(duì)另一個(gè)對(duì)象持有弱引用(使用weak指針)即可。

有一種保留環(huán)的問題相對(duì)隱蔽,出現(xiàn)在使用block的時(shí)候。block是一個(gè)可以被獨(dú)立運(yùn)行的代碼塊,為了保證它隨時(shí)可以被運(yùn)行,它會(huì)持有對(duì)它包含的所有變量的強(qiáng)引用。我們來看有問題的代碼:

@property (nonatomic, strong) ExampleBlock aBlock; // self持有aBlock對(duì)象的強(qiáng)引用

- (void)exampleFunction {
  self.aBlock = ^{
      [self doSomething]; // aBlock持有self的強(qiáng)引用
  };
}

上面的代碼中,self和aBlock兩個(gè)對(duì)象互相持有對(duì)方的強(qiáng)引用,導(dǎo)致了兩個(gè)對(duì)象都無法被釋放。

我們?cè)谟龅缴鲜銮闆r時(shí),要讓其中一個(gè)引用變?yōu)槿跻茫薷暮蟮拇a如下:

@property (nonatomic, strong) ExampleBlock aBlock;

- (void)exampleFunction {
  // 讓block捕獲self的弱引用
  __weak __typeof(self)weakSelf = self; 
  self.aBlock = ^{
    // 把弱引用轉(zhuǎn)化為強(qiáng)引用,防止在block處理過程中self被析構(gòu)。
    __strong __typeof(weakSelf)strongSelf = weakSelf; 
    [strongSelf doSomething];
  }];
}

autorelease

在iOS中,autorelease的意義是稍后釋放。我們先看一段MRR的代碼:

// MRR
- (void)doSomething {
  NSArray* arrayA = [[NSArray alloc] init];
  NSArray* arrayB = [self getEmptyArray];

  // 按照MRR的原則,創(chuàng)建的對(duì)象的地方必須負(fù)責(zé)對(duì)象的釋放。這樣才能保持引用計(jì)數(shù)的平衡。
  // 所以這里必須調(diào)用[arrayA release];來與上面的alloc保持平衡。
  [arrayA release]; 
  // 然而arrayB是從getEmptyArray方法中得來,
  // 我們并不知道getEmptyArray是創(chuàng)建了一個(gè)新的對(duì)象還是返回了某個(gè)類的成員變量,
  // 這里釋放getEmptyArray返回的對(duì)象是不合適的。
}

- (void)getEmptyArray {
  // 我們必須在getEmptyArray的實(shí)現(xiàn)中來保證引用計(jì)數(shù)的平衡。
  // 如果寫return [[[NSArray alloc] init] release];會(huì)返回一個(gè)nil。
  // 所以,用autorelease讓新創(chuàng)建的對(duì)象進(jìn)行稍后釋放。
  return [[[NSArray alloc] init] autorelease];
}

稍后到底是什么時(shí)候?這個(gè)涉及到autoreleasepool的概念。
當(dāng)一個(gè)對(duì)象被autorelease的時(shí)候,它其實(shí)是被注冊(cè)到最里層(autoreleasepool是類似棧的結(jié)構(gòu))的autoreleasepool里,在autoreleasepool被銷毀的時(shí)候,里面所有的對(duì)象都會(huì)被銷毀。
系統(tǒng)會(huì)在每次消息循環(huán)開始的時(shí)候,建立一個(gè)autoreleasepool,在這一次消息循環(huán)結(jié)束后銷毀這個(gè)autoreleasepool。一般情況下,這個(gè)就是最里層的autoreleasepool了。

我們可能會(huì)在一次消息循環(huán)周期內(nèi)創(chuàng)建大量的autorelease對(duì)象。為了防止內(nèi)存占用過多,我們可以手動(dòng)使用@autoreleasepool代碼塊來創(chuàng)建自己的autoreleasepool,讓我們創(chuàng)建的這些對(duì)象提前釋放:

- (void)autoreleasePoolExample {
  @autoreleasepool {
    // 在這個(gè)塊范圍內(nèi)被autorelease的對(duì)象會(huì)加到這個(gè)新的autoreleasepool里,
    // 在這個(gè)代碼塊結(jié)束時(shí)就被釋放。
  }
}

在ARC下我們不能顯式調(diào)用autorelease方法,那autorelease到底會(huì)在什么時(shí)候用到呢?其實(shí)編譯器已經(jīng)幫我們加上了autorelease:

- (void)getEmptyArray {
  // 在ARC下我們不能自己加上autorelease。編譯器為保證平衡和返回值的有效性,
  // 會(huì)給這個(gè)方法的返回值隱式的加上autorelease。
  // 實(shí)際上,除了alloc/new/copy等開頭的方法外,
  // 其它方法的返回值都會(huì)按照這個(gè)規(guī)則,被自動(dòng)加上autorelease。
  return [[NSArray alloc] init];
}

參考文檔

Objective-C高級(jí)編程:iOS與OS X多線程和內(nèi)存管理

Advanced Memory Management Programming Guide
Transitioning to ARC Release Notes
Clang:Objective-C Automatic Reference Counting (ARC)
ARC Best Practices
黑幕背后的Autorelease

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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