自動引用計數,又稱ARC(Automatic Reference Counting)是蘋果在iOS5中引入的重要特性,它減少了我們在內存管理時的麻煩,讓我們可以把更多的精力放在其它更重要的事情上。
雖然ARC給我們帶來了很多方便,但如果開發者不了解基本的內存管理知識,還是會在開發工作中遇到很多問題。所以,我總結了ARC環境下應該知道的內存管理知識,供諸位參考。
基于引用計數的內存管理
要了解ARC,必須先了解Objective-C中對象的內存管理機制以及手動管理引用計數(MRR,Manual Retain-Release)。
Objective-C內存管理機制
Objective-C中的對象都是基于引用計數來管理生命周期的。簡單來說就是,我們在需要持有一個對象時,調用retain讓它的引用計數+1。不需要這個對象的時候,調用release讓它的引用計數-1。當一個對象引用計數為0的時候,這個對象就會被自動銷毀。
MRR
我們在手動管理引用計數的時候,要明確地控制對象的生命周期,顯式的調用每一個retain和release。我們必須清楚的了解每個接口對引用計數的處理(如把一個對象放到數組里引用計數會被+1,用alloc創建的對象的引用計數一開始就是1,用哪些接口創建的對象是已經被調用過autorelease的等等)。在處理引用計數時稍有疏忽,就可能導致程序崩潰或內存泄漏。
ARC
ARC是編譯器通過對代碼的靜態分析,確定對象的生命周期,并在合適的位置自動加上retain和release的機制。把內存管理交給編譯器以后,我們不需要再調用任何的retain和release了。ARC減少了MRR帶來的思考負擔,減少了內存問題出現的可能性,也大幅減少了代碼量。
下圖是蘋果的文檔中對ARC效果的描述,略顯夸張的展示出了ARC的好處。
ARC的內存管理
ARC簡化了引用計數的概念,它把變量對對象的引用分為強引用和弱引用兩種。所有ARC下的內存管理原則都將基于這兩個概念。強引用表示變量擁有對象的所有權。多個變量有可能同時持有同一個對象的強引用。當一個對象沒有被任何變量強引用,它就會被釋放。弱引用表示引用但不擁有對象,它可以避免兩個對象相互強引用導致的內存泄漏問題。
所有權修飾符
ARC下的變量都會被加上下面幾種所有權修飾符,它們指定了變量對其指向對象的所有權形式。整個ARC的規則也正是圍繞著這幾個修飾符運作:__strong:表示對對象的強引用。它是變量修飾符的默認值,也就是說只要沒有顯式的給變量加上所有權修飾符,它就是__strong的。__strong變量在離開作用域范圍后會被廢棄(其實就是編譯器插入了一個release),對對象的強引用也隨之消失。__weak:表示弱引用。當其指向的對象被釋放時,這個變量會被置成nil。__autoreleasing:表示將修飾的對象加入autoreleasepool中,在autoreleasepool被銷毀時自動釋放對對象的強引用。在@autoreleasepool的代碼塊中的變量都會被加上這個修飾符,在超出代碼塊范圍后釋放。詳見下面autorelease的部分。__unsafe_unretained:表示既不持有對象的強引用,也不持有弱引用(對象析構時它不會被置為nil)。正如它的名字描述,它是不安全的。它只是在iOS5之前用來代替__weak。
屬性修飾符
在使用MRR的時候,我們可以給property添加retain、assign、copy這幾種修飾符,來設置想要的內存管理模式,用下面這段代碼來說明這三種修飾符的作用:
// MRR環境
@property (nonatomic, retain) NSObject* retainedObject;
@property (nonatomic, assign) NSObject* assignedObject;
@property (nonatomic, copy) NSMutableString* copiedObject;
- (void)testProperties {
NSObject* objectA = [[NSObject alloc] init]; // objectA引用計數為1。
self.retainedObject = objectA; // self.retainedObject和objectA指向同一個對象,且該對象引用計數為2。
NSObject* objectB = [[NSObject alloc] init]; // objectB引用計數為1。
self.assignedObject = objectB; // self.assignedObject和objectB指向同一個對象,且該對象引用計數為1。
NSMutableString* objectC = [[NSMutableString alloc] initWithFormat:@"test"]; // objectC引用計數為1
self.copiedObject = objectC; // self.copiedObject和objectC指向兩個不同的對象,兩個對象引用計數都為1。
// 這里可能會有疑問,為什么copy的例子用的是NSMutableString而不是NSObject了或者NSString?
// 因為NSObject沒有實現NSCopying協議,沒法復制。
// 而使用NSString會導致self.copiedObject和objectC因為編譯器優化而指向相同的對象。
// 因為是MRR環境,我們要釋放我們自己分配的內存,否則會產生內存泄漏。
[objectA release];
[objectC release];
// 當然,給分配內存的代碼加上autorelease也行:
// NSObject* objectA = [[[NSObject alloc] init] autorelease];
// NSMutableString* objectC = [[[NSMutableString alloc] init] autorelease];
}
- (void)dealloc {
// MRR要在析構時手動清理內存。
[_objectA release];
[_objectB release];
[_objectC release];
[super dealloc];
}
在ARC環境下,增加了兩種新的修飾符:strong和weak,分別對應強引用和弱引用:
// ARC環境
@property (nonatomic, strong) NSObject* strongObject;
@property (nonatomic, weak) NSObject* weakObject;
- (void)testProperties {
NSObject* objectD = [[NSObject alloc] init]; // objectD持有對象D的強引用。
self.strongObject = objectD; // self.strongObject和objectD都持有對象D的強引用
NSObject* objectE = [[NSObject alloc] init]; // objectE持有對象E的強引用。
self.weakObject = objectE; // self.weakObject持有對象E的弱引用
// 在ARC環境下,這個方法執行完后,objectD和objectE對對象D和E的強引用會消失。
// 這時候self.strongObject仍然持有對對象D的強引用。
// self.weakObject之前對對象E持有的是弱引用,對象E析構。self.weakObject的指針被置為nil。
}
// 與MRR不同,ARC環境下,self對象析構時,self.strongObject對對象E的強引用自動消失,對象E自動析構。
Retain Cycle(保留環)也叫循環引用
ARC確實幫助我們避免了許多內存管理的問題。但在ARC環境下,有一類問題還是需要被妥善的處理,這類問題叫做Retain Cycle(保留環)。
ARC環境下的對象在沒有被強引用時就會被釋放,當兩個對象互相對對方持有強引用時,這兩個對象就永遠不會被釋放了。這就導致了上面說的保留環問題。
要解決保留環的思路也簡單,就是理清這兩個對象之間的所有權關系,再讓其中一個對象對另一個對象持有弱引用(使用weak指針)即可。
有一種保留環的問題相對隱蔽,出現在使用block的時候。block是一個可以被獨立運行的代碼塊,為了保證它隨時可以被運行,它會持有對它包含的所有變量的強引用。我們來看有問題的代碼:
@property (nonatomic, strong) ExampleBlock aBlock; // self持有aBlock對象的強引用
- (void)exampleFunction {
self.aBlock = ^{
[self doSomething]; // aBlock持有self的強引用
};
}
上面的代碼中,self和aBlock兩個對象互相持有對方的強引用,導致了兩個對象都無法被釋放。
我們在遇到上述情況時,要讓其中一個引用變為弱引用,修改后的代碼如下:
@property (nonatomic, strong) ExampleBlock aBlock;
- (void)exampleFunction {
// 讓block捕獲self的弱引用
__weak __typeof(self)weakSelf = self;
self.aBlock = ^{
// 把弱引用轉化為強引用,防止在block處理過程中self被析構。
__strong __typeof(weakSelf)strongSelf = weakSelf;
[strongSelf doSomething];
}];
}
autorelease
在iOS中,autorelease的意義是稍后釋放。我們先看一段MRR的代碼:
// MRR
- (void)doSomething {
NSArray* arrayA = [[NSArray alloc] init];
NSArray* arrayB = [self getEmptyArray];
// 按照MRR的原則,創建的對象的地方必須負責對象的釋放。這樣才能保持引用計數的平衡。
// 所以這里必須調用[arrayA release];來與上面的alloc保持平衡。
[arrayA release];
// 然而arrayB是從getEmptyArray方法中得來,
// 我們并不知道getEmptyArray是創建了一個新的對象還是返回了某個類的成員變量,
// 這里釋放getEmptyArray返回的對象是不合適的。
}
- (void)getEmptyArray {
// 我們必須在getEmptyArray的實現中來保證引用計數的平衡。
// 如果寫return [[[NSArray alloc] init] release];會返回一個nil。
// 所以,用autorelease讓新創建的對象進行稍后釋放。
return [[[NSArray alloc] init] autorelease];
}
稍后到底是什么時候?這個涉及到autoreleasepool的概念。
當一個對象被autorelease的時候,它其實是被注冊到最里層(autoreleasepool是類似棧的結構)的autoreleasepool里,在autoreleasepool被銷毀的時候,里面所有的對象都會被銷毀。
系統會在每次消息循環開始的時候,建立一個autoreleasepool,在這一次消息循環結束后銷毀這個autoreleasepool。一般情況下,這個就是最里層的autoreleasepool了。
我們可能會在一次消息循環周期內創建大量的autorelease對象。為了防止內存占用過多,我們可以手動使用@autoreleasepool代碼塊來創建自己的autoreleasepool,讓我們創建的這些對象提前釋放:
- (void)autoreleasePoolExample {
@autoreleasepool {
// 在這個塊范圍內被autorelease的對象會加到這個新的autoreleasepool里,
// 在這個代碼塊結束時就被釋放。
}
}
在ARC下我們不能顯式調用autorelease方法,那autorelease到底會在什么時候用到呢?其實編譯器已經幫我們加上了autorelease:
- (id)getEmptyArray {
// 在ARC下我們不能自己加上autorelease。編譯器為保證平衡和返回值的有效性,
// 會給這個方法的返回值隱式的加上autorelease。
// 實際上,除了alloc/new/copy等開頭的方法外,
// 其它方法的返回值都會按照這個規則,被自動加上autorelease。
return [[NSArray alloc] init];
}
以上是對ARC環境下iOS內存管理總結。