MRC
全稱Manual Reference Counting
,也稱爲MRR
(manual retain-release
),手動引用計數內存管理,即開發者須要手動控制對象的引用計數來管理對象的內存。
在MRC
年代,咱們常常須要寫retain
、release
、autorelease
等方法來手動管理對象內存,然而這些方法在ARC
是禁止調用的,調用會引發編譯報錯。
下面咱們從MRC
提及,聊聊iOS
內存管理。html
應用程序內存管理是在程序運行時分配內存,使用它並在使用完後釋放它的過程。編寫良好的程序將使用盡量少的內存。在 Objective-C 中,它也能夠看做是在許多數據和代碼之間分配有限內存資源全部權的一種方式。掌握內存管理知識,咱們就能夠很好地管理對象生命週期並在再也不須要它們時釋放它們,從而管理應用程序的內存。
雖然一般在單個對象級別上考慮內存管理,但實際上咱們的目標是管理對象圖,要保證在內存中只保留須要用到的對象,確保沒有發生內存泄漏。
下圖是蘋果官方文檔給出的 「內存管理對象圖」,很好地展現了一個對象 「建立——持有——釋放——銷燬」 的過程。 編程
Objective-C 在iOS
中提供了兩種內存管理方法:數組
MRC
,也是本篇文章要講解的內容,咱們經過跟蹤本身持有的對象來顯式管理內存。這是使用一個稱爲 「引用計數」 的模型來實現的,由 Foundation 框架的 NSObject 類與運行時環境一塊兒提供。緩存
ARC
,系統使用與MRC
相同的引用計數系統,可是它會在編譯時爲咱們插入適當的內存管理方法調用。使用ARC
,咱們一般就不須要了解本文章中描述的MRC
的內存管理實現,儘管在某些狀況下它可能會有所幫助。可是,做爲一名合格的iOS
開發者,掌握這些知識是頗有必要的。安全
不正確的內存管理致使的問題主要有兩種:
① 釋放或覆蓋仍在使用的數據
這會致使內存損壞,而且一般會致使應用程序崩潰,甚至損壞用戶數據。
② 不釋放再也不使用的數據會致使內存泄漏
內存泄漏是指沒有釋放已分配的內存,即便它再也不被使用。內存泄漏會致使應用程序不斷增長內存使用量,進而可能致使系統性能降低或應用程序被終止。網絡
可是,從引用計數的角度考慮內存管理一般會拔苗助長,由於你會傾向於根據實現細節而不是實際目標來考慮內存管理。相反,你應該從對象全部權和對象圖的角度考慮內存管理。多線程
Cocoa 使用簡單的命名約定來指示你什麼時候持有由方法返回的對象。(請參閱 《內存管理策略》
章節)app
儘管內存管理基本策略很簡單,可是你能夠採起一些措施來簡化內存管理,並幫助確保程序保持可靠和健壯,同時最大程度地減小其資源需求。(請參閱 《實用內存管理》
章節)框架
自動釋放池塊提供了一種機制,你能夠經過該機制向對象發送 「延遲」release
消息。這在須要放棄對象全部權但又但願避免當即釋放對象的狀況下頗有用(例如從方法返回對象時)。在某些狀況下,你可能會使用本身的自動釋放池塊。(請參閱 《使用 Autorelease Pool Blocks》
章節)工具
爲了在編譯時發現代碼問題,可使用 Xcode 內置的 Clang Static Analyzer。 若是仍然出現內存管理問題,則可使用其餘工具和技術來識別和診斷問題。
《Technical Note TN2239, iOS Debugging Magic》 中描述了許多工具和技術,尤爲是使用NSZombie
(殭屍對象)來幫助查找過分釋放的對象。
您可使用 Instruments 來跟蹤引用計數事件並查找內存泄漏。請參閱 《Instruments Help》。
NSObject 協議中定義的內存管理方法與遵照這些方法命名約定的自定義方法的組合提供了用於引用計數環境中的內存管理的基本模型。NSObject 類還定義了一個dealloc
方法,該方法在對象被銷燬時自動調用。
在MRC
下,咱們要嚴格遵照引用計數內存管理規則。
內存管理模型基於對象全部權。任何對象均可以擁有一個或多個全部者。只要一個對象至少擁有一個全部者,它就會繼續存在。若是對象沒有全部者,則運行時系統會自動銷燬它。爲了確保你清楚本身什麼時候擁有和不擁有對象的全部權,Cocoa 設置瞭如下策略:
alloc/new/copy/mutableCopy
等方法(或者以這些方法名開頭的方法)建立的對象咱們直接持有,其RC
(引用計數,如下使用統一使用RC
)初始值爲 1,咱們直接使用便可,在不須要使用的時候調用一下release
方法進行釋放。id obj = [NSObject alloc] init]; // 建立並持有對象,RC = 1
/* * 使用該對象,RC = 1 */
[obj release]; // 在不須要使用的時候調用 release,RC = 0,對象被銷燬
複製代碼
若是咱們經過自定義方法 建立並持有對象,則方法名應該以 alloc/new/copy/mutableCopy
開頭,且應該遵循駝峯命名法規則,返回的對象也應該由這些方法建立,如:
- (id)allocObject
{
id obj = [NSObject alloc] init];
retain obj;
}
複製代碼
能夠經過retainCount
方法查看對象的引用計數值。
NSLog(@"%ld", [obj retainCount]);
複製代碼
retain
對一個對象進行持有。使用上述方法之外的方法建立的對象,咱們並不持有,其RC
初始值也爲 1。可是須要注意的是,若是要使用(持有)該對象,須要先進行retain
,不然可能會致使程序Crash
。緣由是這些方法內部是給對象調用了autorelease
方法,因此這些對象會被加入到自動釋放池中。 ① 狀況一:iOS 程序中不手動指定@autoreleasepool
當RunLoop
迭代結束時,會自動給自動釋放池中的對象調用release
方法。因此若是咱們使用前不進行retain
,若是RunLoop
迭代結束,對象調用release
方法其RC
值就會變成 0,該對象就會被銷燬。若是咱們這時候訪問已經被銷燬的對象,程序就會Crash
。
/* 正確的用法 */
id obj = [NSMutableArray array]; // 建立對象但並不持有,對象加入自動釋放池,RC = 1
[obj retain]; // 使用以前進行 retain,對對象進行持有,RC = 2
/* * 使用該對象,RC = 2 */
[obj release]; // 在不須要使用的時候調用 release,RC = 1
/* * RunLoop 可能在某一時刻迭代結束,給自動釋放池中的對象調用 release,RC = 0,對象被銷燬 * 若是這時候 RunLoop 還未迭代結束,該對象還能夠被訪問,不過這是很是危險的,容易致使 Crash */
複製代碼
② 狀況二:手動指定@autoreleasepool
這種狀況就更加明顯了,若是@autoreleasepool
做用域結束,就會自動給autorelease
對象調用release
方法。若是這時候咱們再訪問該對象,程序就會崩潰EXC_BAD_ACCESS
。
/* 錯誤的用法 */
id obj;
@autoreleasepool {
obj = [NSMutableArray array]; // 建立對象但並不持有,對象加入自動釋放池,RC = 1
} // @autoreleasepool 做用域結束,對象 release,RC = 0,對象被銷燬
NSLog(@"%@",obj); // EXC_BAD_ACCESS
複製代碼
/* 正確的用法 */
id obj;
@autoreleasepool {
obj = [NSMutableArray array]; // 建立對象但並不持有,對象加入自動釋放池,RC = 1
[obj retain]; // RC = 2
} // @autoreleasepool 做用域結束,對象 release,RC = 1
NSLog(@"%@",obj); // 正常訪問
/* * 使用該對象,RC = 1 */
[obj release]; // 在不須要使用的時候調用 release,RC = 0,對象被銷燬
複製代碼
若是咱們經過自定義方法 建立但並不持有對象,則方法名就不該該以 alloc/new/copy/mutableCopy
開頭,且返回對象前應該要先經過autorelease
方法將該對象加入自動釋放池。如:
- (id)object
{
id obj = [NSObject alloc] init];
[obj autorelease];
retain obj;
}
複製代碼
這樣調用方在使用該方法建立對象的時候,他就會知道他不持有該對象,因而他會在使用該對象前進行retain
,並在不須要該對象時進行release
。
備註:
release
和autorelease
的區別:
- 調用
release
,對象的RC
會當即 -1;- 調用
autorelease
,對象的RC
不會當即 -1,而是將對象添加進自動釋放池,它會在一個恰當的時刻自動給對象調用release
,因此autorelease
至關於延遲了對象的釋放。
再也不須要本身持有的對象時釋放
在不須要使用(持有)對象的時候,須要調用一下release
或者autorelease
方法進行釋放(或者稱爲 「放棄對象使用權」),使其RC
-1,防止內存泄漏。當對象的RC
爲 0 時,就會調用dealloc
方法銷燬對象。
不能釋放非本身持有的對象
從以上咱們能夠得知,持有對象有兩種方式,一是經過 alloc/new/copy/mutableCopy
等方法建立對象,二是經過retain
方法。若是本身是持有者,那麼在不須要該對象的時候須要調用一下release
方法進行釋放。可是,若是本身不是持有者,就不能對對象進行release
,不然會發生程序崩潰EXC_BAD_ACCESS
,以下兩種狀況:
id obj = [[NSObject alloc] init]; // 建立並持有對象,RC = 1
[obj release]; // 若是本身是持有者,在不須要使用的時候調用 release,RC = 0
/* * 此時對象已被銷燬,不該該再對其進行訪問 */
[obj release]; // EXC_BAD_ACCESS,這時候本身已經不是持有者,再 release 就會 Crash
/* * 再次 release 已經銷燬的對象(過分釋放),或是訪問已經銷燬的對象都會致使崩潰 */
複製代碼
id obj = [NSMutableArray array]; // 建立對象,但並不持有對象,RC = 1
[obj release]; // EXC_BAD_ACCESS 雖然對象的 RC = 1,可是這裏並不持有對象,因此致使 Crash
複製代碼
還有一種狀況,這是不容易發現問題的狀況。下面程序運行竟然不會崩潰?這是爲何呢?這裏要介紹兩個概念,野指針
和殭屍對象
。
- 野指針: 在 C 中是指沒有進行初始化的指針,該指針指向一個隨機的空間,它的值是個垃圾值;在 OC 中是指指向的對象已經被回收了的指針(網上不少都是這樣解釋,但我認爲它應該叫 「懸垂指針」 纔對)。
- 殭屍對象: 指已經被銷燬的對象,但這個對象所佔的內存空間尚未分配給別人。
以下這種狀況,當咱們經過野指針
去訪問殭屍對象
的時候,可能會有問題,也可能沒有問題。若是殭屍對象所佔的空間尚未分配給別人,這時候訪問沒有問題,若是已經分配給了別人,再次訪問就會崩潰。
Person *person = [[Person alloc] init]; // 建立並持有對象,RC = 1
[person release]; // 若是本身是持有者,在不須要使用的時候調用 release,RC = 0
[person release]; // 這時候 person 指針爲野指針,對象爲殭屍對象
[person release]; // 可能這時候殭屍對象所佔的空間尚未分配給別人,因此能夠正常訪問
複製代碼
以上幾個例子均可以用一句話總結:不能釋放非本身持有的對象。
以上就是內存管理基本的四條規則,你對照上篇文章中講的《辦公室裏的照明問題》,是否是就比較好理解了,你細品,你細細的品!
Person 對象是使用alloc
方法建立的,所以在不須要該對象時發送一條release
消息。
{
Person *aPerson = [[Person alloc] init];
// ...
NSString *name = aPerson.fullName;
// ...
[aPerson release];
}
複製代碼
當你須要發送延遲release
消息時,可使用autorelease
,一般用在從方法返回對象時。例如,你能夠像這樣實現 fullName 方法:
- (NSString *)fullName {
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName] autorelease];
return string;
}
複製代碼
根據內存管理規則,你經過alloc
方法建立並持有對象,要在不須要該對象時發送一條release
消息。可是若是你在方法中使用release
,則return
以前就會銷燬 NSString 對象,該方法將返回無效對象。使用autorelease
,就會延遲release
,在 NSString 對象被釋放以前返回。
你還能夠像這樣實現 fullName 方法:
- (NSString *)fullName {
NSString *string = [NSString stringWithFormat:@"%@ %@",
self.firstName, self.lastName];
return string;
}
複製代碼
根據內存管理規則,你不持有 NSString 對象,所以你不用擔憂它的釋放,直接return
便可。stringWithFormat 方法內部會給 NSString 對象調用autorelease
方法。
相比之下,如下實現是錯誤的:
- (NSString *)fullName {
NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName];
return string;
}
複製代碼
在 fullName 方法內部咱們經過alloc
方法建立對象並持有,然而並無釋放對象。而該方法名不以 alloc/new/copy/mutableCopy
等開頭。在調用方看來,經過該方法得到的對象並不持有,所以他會進行retain
並在他不須要該對象時release
,在他看來這樣使用該對象沒有內存問題。然而這時候該對象的引用計數爲 1,並無銷燬,就發生了內存泄漏。
Cocoa 中的一些方法指定經過引用返回對象(它們採用ClassName **
或id *
類型的參數)。常見的就是使用NSError
對象,該對象包含有關錯誤的信息(若是發生錯誤),如initWithContentsOfURL:options:error:
(NSData
)和initWithContentsOfFile:encoding:error:
(NSString
)方法等。
在這些狀況下,也聽從內存管理規則。當你調用這些方法時,你不會建立該NSError
對象,所以你不持有該對象,也無需釋放它,如如下示例所示:
NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
// Deal with error...
}
// ...
[string release];
複製代碼
NSObject 類定義了一個dealloc
方法,該方法會在一個對象沒有全部者(RC
=0)而且它的內存被回收時由系統自動調用 —— 在 Cocoa 術語中稱爲freed
或deallocated
。 dealloc
方法的做用是銷燬對象自身的內存,並釋放它持有的任何資源,包括任何實例變量的全部權。
如下舉了一個在 Person 類中實現 dealloc
方法的示例:
@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
@implementation Person
// ...
- (void)dealloc
[_firstName release];
[_lastName release];
[super dealloc];
}
複製代碼
注意:
- 切勿直接調用另外一個對象
dealloc
的方法;- 你必須在實現結束時調用
[super dealloc]
;- 你不該該將系統資源的管理與對象生命週期聯繫在一塊兒,請參閱
《不要使用 dealloc 管理稀缺資源》
章節;- 當應用程序終止時,可能不會向對象發送
dealloc
消息。由於進程的內存在退出時會自動清除,因此讓操做系統清理資源比調用全部對象的dealloc
方法更有效。
Core Foundation 對象有相似的內存管理規則(請參閱 《 Core Foundation 內存管理編程指南》)。可是,Cocoa 和 Core Foundation 的命名約定不一樣。特別是 Core Foundation 的建立對象的規則(請參閱 《The Create Rule》)不適用於返回 Objective-C 對象的方法。例如如下的代碼片斷,你不負責放棄 myInstance 的全部權。由於在 Cocoa 中使用 alloc/new/copy/mutableCopy
等方法(或者以這些方法名開頭的方法)建立的對象,咱們才須要對其進行釋放。
MyClass * myInstance = [MyClass createInstance];
複製代碼
儘管內存管理基本策略很簡單,可是你能夠採起一些措施來簡化內存管理,並幫助確保程序保持可靠和健壯,同時最大程度地減小其資源需求。
若是類中有對象類型的屬性,則你必須確保在使用過程當中該屬性賦值的對象不被釋放。所以,在賦值對象時,你必須持有對象的全部權,讓其引用計數加 1。還必需要把當前持有的舊對象的引用計數減 1。
有時它可能看起來很乏味或繁瑣,但若是你始終使用訪問器方法,那麼內存管理出現問題的可能性會大大下降。若是你在整個代碼中對實例變量使用retain
和release
,這確定是錯誤的作法。
如下在 Counter 類中定義了一個NSNumber
對象屬性。
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;
複製代碼
@property
會自動生成setter
和getter
方法的聲明,一般,你應該使用@synthesize
讓編譯器合成方法。但若是咱們瞭解訪問器方法的實現是有益的。
@synthesize
會自動生成setter
和getter
方法的實現以及下劃線實例變量,詳細的解釋將在下一篇ARC
文章中講到。
getter
方法只須要返回合成的實例變量,因此不用進行retain
和release
。
- (NSNumber *)count {
return _count;
}
複製代碼
setter
方法中,若是其餘全部人都遵循相同的規則,那麼其餘人極可能隨時讓新對象 newCount 的引用計數減 1,從而致使 newCount 被銷燬,因此你必須對其retain
使其引用計數加 1。你還必須對舊對象release
以放棄對它的持有。因此,先對新對象進行retain
,再對舊對象進行release
,而後再進行賦值操做。(在Objective-C
中容許給nil
發送消息,且這樣會直接返回不作任何事情。因此就算是第一次調用,_count 變量爲nil
,對其進行 release
也沒事。能夠參閱《深刻淺出 Runtime(三):消息機制》)
注意: 你必須先對新對象進行
retain
,再對舊對象進行release
。順序顛倒的話,若是新舊對象是同一對象,則可能會發生意外致使對象dealloc
。
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// Make the new assignment.
_count = newCount;
}
複製代碼
以上是蘋果官方的作法,該作法在性能上略有不足,若是新舊對象是同一個對象,就存在沒必要要的方法調用。
更好的作法以下:先判斷新舊對象是不是同一個對象,若是是的話就什麼都不作;若是新舊對象不是同一個對象,則對舊對象進行release
,對新對象進行retain
並賦值給合成的實例變量。
- (void)setCount:(NSNumber *)newCount {
if (_count != newCount) {
[_count release];
_count = [newCount retain];
}
}
複製代碼
假設咱們要重置以上count
屬性的值。有如下兩種方法:
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release];
}
複製代碼
- (void)reset {
NSNumber *zero = [NSNumber numberWithInteger:0];
[self setCount:zero];
}
複製代碼
對於簡單的狀況,咱們還能夠像下面這樣直接操做_count
變量,但這樣作早晚會發生錯誤(例如,當你忘記retain
或release
,或者實例變量的內存管理語義(即屬性關鍵字)發生更改時)。
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[_count release];
_count = zero;
}
複製代碼
另外請注意,若是使用KVO
,則以這種方式更改變量不會觸發KVO
監聽方法。關於KVO
我作了比較全面的總結,能夠參閱《iOS - 關於 KVO 的一些總結》。
你不該該在初始化方法和dealloc
中使用訪問器方法來設置實例變量,而是應該直接操做實例變量。
例如,咱們要在初始化 Counter 對象時,初始化它的count
屬性。正確的作法以下:
- init {
self = [super init];
if (self) {
_count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}
複製代碼
- initWithCount:(NSNumber *)startingCount {
self = [super init];
if (self) {
_count = [startingCount copy];
}
return self;
}
複製代碼
因爲 Counter 類具備實例變量,所以還必須實現dealloc
方法。在該方法中經過向它們發送release
消息來放棄任何實例變量的全部權,並在最後調用super
的實現:
- (void)dealloc {
[_count release];
[super dealloc];
}
複製代碼
以上是蘋果官方的作法。推薦作法以下,在release
以後再對 _count 賦值nil
。
備註:
先解釋一下nil
和release
的做用:nil
是將一個對象的指針置爲空,只是切斷了指針和內存中對象的聯繫,並無釋放對象內存;而release
纔是真正釋放對象內存的操做。
之因此在release
以後再對 _count 賦值nil
,是爲了防止 _count 在被銷燬以後再次被訪問而致使Crash
。
- (void)dealloc {
[_count release];
_count = nil;
[super dealloc];
}
複製代碼
咱們也能夠在dealloc
經過self.count = nil;
一步到位,由於一般它至關於[_count release];
和_count = nil;
兩步操做。可是蘋果說了,不建議咱們在dealloc
中使用訪問器方法。
- (void)dealloc {
self.count = nil;
[super dealloc];
}
複製代碼
Why? 爲何初始化方法中須要
self = [super init]
?
- 先大概解釋一下
self
和super
。self
是對象指針,指向當前消息接收者。super
是編譯器指令,使用super
調用方法是從當前消息接收者類的父類中開始查找方法的實現,但消息接收者仍是子類。有關self
和super
的詳細解釋能夠參閱《深刻淺出 Runtime(四):super 的本質》。- 調用
[super init]
,是子類去調用父類的init
方法,先完成父類的初始化工做。要注意調用過程當中,父類的init
方法中的self
仍是子類。- 執行
self = [super init]
,若是父類初始化成功,接下來就進行子類的初始化;若是父類初始化失敗,則[super init]
會返回nil
並賦值給self
,接下來if (self)
語句的內容將不被執行,子類的init
方法也返回nil
。這樣作能夠防止由於父類初始化失敗而返回了一個不可用的對象。若是你不是這樣作,你可能你會獲得一個不可用的對象,而且它的行爲是不可預測的,最終可能會致使你的程序發生Crash
。
Why? 爲何不要在初始化方法和 dealloc 中使用訪問器方法?
- 在初始化方法和
dealloc
中,對象的存在與否還不肯定,它可能還未初始化完畢,因此給對象發消息可能不會成功,或者致使一些問題的發生。
- 進一步解釋,假如咱們在
init
中使用setter
方法初始化實例變量。在init
中,咱們會調用self = [super init]
對父類的東西先進行初始化,即子類先調用父類的init
方法(注意: 調用的父類的init
方法中的self
仍是子類對象)。若是父類的init
中使用setter
方法初始化實例變量,且子類重寫了該setter
方法,那麼在初始化父類的時候就會調用子類的setter
方法。而此時只是在進行父類的初始化,子類初始化還未完成,因此可能會發生錯誤。- 在銷燬子類對象時,首先是調用子類的
dealloc
,最後調用[super dealloc]
(這與init
相反)。若是在父類的dealloc
中調用了setter
方法且該方法被子類重寫,就會調用到子類的setter
方法,但此時子類已經被銷燬,因此這也可能會發生錯誤。- 在 《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》書中的第 31 條 —— 在 dealloc 方法中只釋放引用並解除監聽 一文中也提到:在 dealloc 裏不要調用屬性的存取方法,由於有人可能會覆寫這些方法,並於其中作一些沒法在回收階段安全執行的操做。此外,屬性可能正處於 「鍵值觀測」(Key-Value Observation,KVO)機制的監控之下,該屬性的觀察者(observer)可能會在屬性值改變時 「保留」 或使用這個即將回收的對象。這種作法會令運行期系統的狀態徹底失調,從而致使一些莫名其妙的錯誤。
- 綜上,錯誤的緣由由繼承和子類重寫訪問器方法引發。在初始化方法和 dealloc 中使用訪問器方法的話,若是存在繼承且子類重寫了訪問器方法,且在方法中作了一些其它操做,就頗有可能發生錯誤。雖然通常狀況下咱們可能不會同時知足以上條件而致使錯誤,可是爲了不錯誤的發生,咱們仍是規範編寫代碼比較好。
- 性能降低。特別是,若是屬性是
atomic
的。- 可能產生反作用。如使用
KVO
的話會觸發KVO
等。不過,有些狀況咱們必須破例。好比:
- 待初始化的實例變量聲明在父類中,而咱們又沒法在子類中訪問此實例變量的話,那麼咱們在初始化方法中只能經過
setter
來對實例變量賦值。
retain
對象會建立對該對象的強引用(即引用計數 +1)。一個對象在release
它的全部強引用以後(即引用計數 =0)纔會dealloc
。若是兩個對象相互retain
強引用,或者多個對象,每一個對象都強引用下一個對象直到回到第一個,就會出現 「Retain Cycles
(循環引用)」 問題。循環引用會致使它們中的任何對象都沒法dealloc
,就產生了內存泄漏。
舉個例子,Document 對象中有一個屬性 Page 對象,每一個 Page 對象都有一個屬性,用於存儲它所在的 Document。若是 Document 對象具備對 Page 對象的強引用,而且 Page 對象具備對 Document 對象的強引用,則它們都不能被銷燬。
「Retain Cycles
」 問題的解決方案是使用弱引用。弱引用是非持有關係,對象do not retain
它引用的對象。
在
MRC
中,這裏的 「弱引用」 是指do not retain
,而不是ARC
中的weak
。
可是,爲了保持對象圖無缺無損,必須在某處有強引用(若是隻有弱引用,則 Page 對象和 Paragraph 對象可能沒有任何全部者,所以將被銷燬)。所以,Cocoa 創建了一個約定,即父對象應該對其子對象保持強引用(retain
),而子對象應該對父對象保持弱引用(do not retain
)。
所以,Document 對象具備對其 Page 對象的強引用,但 Page 對象對 Document 對象是弱引用,以下圖所示:
Cocoa 中弱引用的示例包括但不限於 table data sources、outline view items、notification observers 以及其餘 targets 和 delegates。
當你向只持有弱引用的對象發送消息時,須要當心。若是在對象銷燬後向其發送消息就會Crash
。你必須定義好何時對象是有效的。在大多數狀況下,弱引用對象知道其它對象對它的弱引用,就像循環引用的狀況同樣,你要負責在弱引用對象銷燬時通知其它對象。例如,當你向通知中心註冊對象時,通知中心會存儲對該對象的弱引用,並在發佈相應的通知時向其發送消息。在對象要銷燬時,你須要在通知中心註銷它,以防止通知中心向已銷燬的對象發送消息。一樣,當 delegate 對象銷燬時,你須要向委託對象發送setDelegate: nil
消息來刪除 delegate 引用。這些消息一般在對象的 dealloc 方法中發送。
Cocoa 的全部權策略指定,對象做爲方法參數傳入,其在調用的方法的整個範圍內保持有效,也能夠做爲方法的返回值返回,而沒必要擔憂它被釋放。對於應用程序來講,對象的 getter 方法返回緩存的實例變量或計算值並不重要。重要的是對象在你須要的時間內保持有效。
此規則偶爾會有例外狀況,主要分爲兩類。
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.
複製代碼
當一個對象從一個基本集合類中移除時,它將被髮送一條release
(而不是autorelease
)消息。若是集合是移除對象的惟一全部者,則移除的對象(示例中的 heisenObject)將當即被銷燬。
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
複製代碼
在某些狀況下,你經過父對象得到子對象,而後直接或間接release
父對象。若是release
父對象致使它被銷燬,而且父對象是子對象的惟一全部者,則子對象(示例中的 heisenObject)將同時被銷燬(假設在父對象的dealloc
方法中,子對象被髮送一個release
而不是一個autorelease
消息)。
爲了防止這些狀況發生,在獲得 heisenObject 時retain
它,並在完成後release
它。例如:
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];
複製代碼
你一般不該該在dealloc
方法中管理稀缺資源,如文件描述符,網絡鏈接和緩衝區或緩存等。特別是,你不該該設計類,以便在你想讓系統調用dealloc
時就調用它。因爲bug
或應用程序崩潰,dealloc
的調用可能會被延遲或未調用。
相反,若是你有一個類的實例管理稀缺的資源,你應該在你再也不須要這些資源時讓該實例釋放這些資源。而後,你一般會release
該實例,緊接着它dealloc
。若是該實例的dealloc
沒有被及時調用或者未調用,你也不會遇到稀缺資源不被及時釋放或者未釋放的問題,由於此前你已經釋放了資源。
若是你嘗試在dealloc
上進行資源管理,則可能會出現問題。例如:
依賴對象圖的釋放機制。
對象圖的釋放機制本質上是無序的。儘管一般你但願能夠按照特定的順序釋放,可是會讓程序變得很脆弱。若是對象被autorelease
而不是release
,則釋放順序可能會改變,這可能會致使意外的結果。
不回收稀缺資源。
內存泄漏是應該被修復的bug
,但它們一般不會當即致命。然而,若是在你但願釋放稀缺資源時沒有釋放,則可能會遇到更嚴重的問題。例如,若是你的應用程序用完了文件描述符,則用戶可能沒法保存數據。
釋放資源的操做被錯誤的線程執行。
若是一個對象在一個意外的時間調用了autorelease
,它將在它碰巧進入的任何一個線程的自動釋放池塊中被釋放。對於只能從一個線程觸及的資源來講,這很容易致命。
將對象添加到集合(例如array
,dictionary
或set
)時,集合將得到對象的全部權。當從集合中移除對象或集合自己被銷燬時,集合將放棄對象的全部權。所以,例如,若是要建立一個存儲numbers
的數組,能夠執行如下任一操做:
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
[array addObject:convenienceNumber];
}
複製代碼
在這種狀況下,NSNumber
對象不是經過alloc
等建立,所以無需調用release
。也不須要對NSNumber
對象進行retain
,由於數組會這樣作。
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
[array addObject:allocedNumber];
[allocedNumber release];
}
複製代碼
在這種狀況下,你就須要對NSNumber
對象進行release
。數組會在addObject:
時對NSNumber
對象進行retain
,所以在數組中它不會被銷燬。
要理解這一點,能夠站在實現集合類的人的角度。你要確保在集合中它們不會被銷燬,因此你在它們添加進集合時給它們發送一個retain
消息。若是刪除了它們,則必須給它們發送一個release
消息。在集合的dealloc
方法中,應該向集合中全部剩餘的對象發送一條release
消息。
全部權策略經過引用計數實現的,引用計數也稱爲「retain count
」。每一個對象都有一個retain count
。
retain count
爲 1。retain
消息時,其retain count
將 +1。release
消息時,其retain count
將 -1。autorelease
消息時,其retain count
在當前自動釋放池塊結束時 -1。retain count
減小到 0,它將dealloc
。重要提示: 不該該顯式詢問對象的
retain count
是多少。結果每每會產生誤導,由於你可能不知道哪些系統框架對象retain
了你關注的對象。在調試內存管理問題時,你只須要遵照內存管理規則就好了。
備註: 關於這些方法的具體實現,請參閱
《iOS - 老生常談內存管理(四):源碼分析內存管理方法》
。
自動釋放池塊提供了一種機制,讓你能夠放棄對象的全部權,但避免當即釋放它(例如從方法返回對象時)。一般,你不須要建立本身的自動釋放池塊,但在某些狀況下,你必須這樣作或者這樣作是有益的。
Autorelease Pool Blocks 使用@autoreleasepool
標記,示例以下:
@autoreleasepool {
// Code that creates autoreleased objects.
}
複製代碼
在@autoreleasepool
的末尾,在塊中接收到autorelease
消息的對象將被髮送一條release
消息。對象在塊內每接收一次autorelease
消息,就會被髮送一條release
消息。 與任何其餘代碼塊同樣,@autoreleasepool
能夠嵌套,可是你一般不會這樣作。
@autoreleasepool {
// . . .
@autoreleasepool {
// . . .
}
. . .
}
複製代碼
在MRC
下還可使用NSAutoreleasePool
建立自動釋放池。不過建議使用@autoreleasepool
,蘋果說它比NSAutoreleasePool
快大約六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release]; // [pool drain]
複製代碼
Cocoa 老是但願代碼在@autoreleasepool
中執行,不然autorelease
對象不會被release
,致使內存泄漏。若是你在@autoreleasepool
以外發送autorelease
消息,Cocoa 會打印一個合適的錯誤消息。AppKit 和 UIKit 框架會在RunLoop
每次事件循環迭代中建立並處理@autoreleasepool
,所以,你一般沒必要本身建立@autoreleasepool
,甚至不須要知道建立@autoreleasepool
的代碼怎麼寫。
可是,有三種狀況可能會使用你本身的@autoreleasepool
:
@autoreleasepool
在每次循環結束時銷燬這些對象。這樣能夠減小應用程序的最大內存佔用。@autoreleasepool
;不然,你的應用程序將存在內存泄漏。(有關詳細信息,請參閱《Autorelease Pool Blocks 和線程》
章節。關於
@autoreleasepool
的底層原理,能夠參閱《iOS - 聊聊 autorelease 和 @autoreleasepool》。
許多程序建立autorelease
的臨時對象。這些對象將添加到程序的內存佔用空間,直到塊結束。在許多狀況下,容許臨時對象累積直到當前事件循環迭代結束時,而不會致使過多的開銷。可是,在某些狀況下,你可能會建立大量臨時對象,這些對象會大大增長內存佔用,而且你但願更快地銷燬這些對象。在這時候,你就能夠建立本身的@autoreleasepool
。在塊結束時,臨時對象被release
,這可讓它們儘快dealloc
,從而減小程序的內存佔用。
如下示例演示瞭如何在 for 循環中使用 local autorelease pool block。
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}
複製代碼
for 循環一次處理一個文件。在@autoreleasepool
內發送autorelease
消息的任何對象(例如 fileContents)在塊結束時release
。
在@autoreleasepool
以後,你應該將塊中任何autorelease
對象視爲 「已銷燬」。不要向該對象發送消息或將其返回給你的方法調用者。若是你須要某個autorelease
的臨時對象在@autoreleasepool
結束以後依然可用,能夠經過在塊內對該對象發送retain
消息,而後在塊以後將對其發送autorelease
,以下示例所示:
– (id)findMatchingObject:(id)anObject {
id match;
while (match == nil) {
@autoreleasepool {
/* Do a search that creates a lot of temporary objects. */
match = [self expensiveSearchForObject:anObject];
if (match != nil) {
[match retain]; /* Keep match around. */
}
}
}
return [match autorelease]; /* Let match go and return it. */
}
複製代碼
在@autoreleasepool
中給match
對象發送一條retain
消息,並在@autoreleasepool
以後給其發送一條autorelease
消息,延長了match
對象的生命週期,容許它在while
循環外接收消息,而且能夠返回給findMatchingObject:
方法的調用方。
Cocoa 應用程序中的每一個線程都維護本身的 autorelease pool blocks 棧。若是你寫的是一個僅基於 Foundation 的程序或者若是你使用子線程,則須要建立本身的@autoreleasepool
。 若是你的應用程序或線程長期存在而且可能會產生大量的autorelease
對象,則應使用@autoreleasepool
(如 AppKit 和 UIKit 就在主線程建立了@autoreleasepool
);不然,autorelease
對象會不斷累積,致使你的內存佔用量不斷增長。若是你在子線程上沒有進行 Cocoa 調用,則不須要使用@autoreleasepool
。
注意: 若是你使用
pthread
(POSIX thread
)而不是使用NSThread
建立子線程,那麼你就不能使用 Cocoa 除非 Cocoa 處於多線程模式。Cocoa 只有在detach
它的第一個NSThread
對象以後纔會進入多線程模式。要想在pthread
建立的子線程上使用 Cocoa,你的應用程序必須先detach
至少一個能夠當即退出的NSThread
對象。你可使用NSThread
的類方法isMultiThreaded
測試 Cocoa 是否處於多線程模式。