Objective-C 內存管理——你需要知道的一切
前言
有關 iOS 內存管理的文章相信大家都看過不少了,我自己也一樣。不過網上大部分文章都沒有解決對于內存管理的一些關鍵性的疑惑,對于初學者來說并不是很友好。本文旨在從初學者的角度出發,對 iOS 內存管理的關鍵部分進行介紹,希望能對廣大 iOS 學習者有所幫助。
本文首發于我發起的 筆試面試知識整理 項目,這里是對應的 Github 倉庫 ,由多人協作編輯而成。如發現有錯誤的地方,隨時歡迎指正。
Objective-C 中的內存分配
在 Objective-C 中,對象通常是使用 alloc 方法在堆上創建的。 [NSObject alloc] 方法會在對堆上分配一塊內存,按照 NSObject 的內部結構填充這塊兒內存區域。
一旦對象創建完成,就不可能再移動它了。因為很可能有很多指針都指向這個對象,這些指針并沒有被追蹤。因此沒有辦法在移動對象的位置之后更新全部的這些指針。
MRC 與 ARC
Objective-C中提供了兩種內存管理機制:MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分別提供對內存的手動和自動管理,來滿足不同的需求。現在蘋果推薦使用 ARC 來進行內存管理。
MRC
對象操作的四個類別
| 對象操作 | OC中對應的方法 | 對應的 retainCount 變化 |
|---|---|---|
| 生成并持有對象 | alloc/new/copy/mutableCopy等 | +1 |
| 持有對象 | retain | +1 |
| 釋放對象 | release | -1 |
| 廢棄對象 | dealloc | -1 |
注意:這些對象操作的方法其實并不包括在OC中,而是包含在Cocoa框架下的Foundation框架中。
四個法則
-
自己生成的對象,自己持有。
-
非自己生成的對象,自己也能持有。
-
不在需要自己持有的對象的時候,釋放。
-
非自己持有的對象無法釋放。
如下是四個黃金法則對應的代碼示例:
/*
* 自己生成并持有該對象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
/*
* 持有非自己生成的對象
*/
id obj = [NSArray array]; // 非自己生成的對象,且該對象存在,但自己不持有
[obj retain]; // 自己持有對象
/*
* 不在需要自己持有的對象的時候,釋放
*/
id obj = [[NSObeject alloc] init]; // 此時持有對象
[obj release]; // 釋放對象
/*
* 指向對象的指針仍就被保留在obj這個變量中
* 但對象已經釋放,不可訪問
*/
/*
* 非自己持有的對象無法釋放
*/
id obj = [NSArray array]; // 非自己生成的對象,且該對象存在,但自己不持有
[obj release]; // 此時將運行時crash 或編譯器報error
其中 非自己生成的對象,且該對象存在,但自己不持有 這個特性是使用 autorelease 來實現的,示例代碼如下:
- (id) getAObjNotRetain {
id obj = [[NSObject alloc] init]; // 自己持有對象
[obj autorelease]; // 取得的對象存在,但自己不持有該對象
return obj;
}
autorelease 使得對象在超出生命周期后能正確的被釋放(通過調用release方法)。在調用 release 后,對象會被立即釋放,而調用 autorelease 后,對象不會被立即釋放,而是注冊到 autoreleasepool 中,經過一段時間后 pool 結束,此時調用release方法,對象被釋放。
在MRC的內存管理模式下,與對變量的管理相關的方法有:retain, release 和 autorelease。retain 和 release 方法操作的是引用記數,當引用記數為零時,便自動釋放內存。并且可以用 NSAutoreleasePool 對象,對加入自動釋放池(autorelease 調用)的變量進行管理,當 drain 時回收內存。
ARC
ARC 是蘋果引入的一種自動內存管理機制,會根據引用計數自動監視對象的生存周期,實現方式是在編譯時期自動在已有代碼中插入合適的內存管理代碼以及在 Runtime 做一些優化。
變量標識符
在ARC中與內存管理有關的變量標識符,有下面幾種:
-
__strong
-
__weak
-
__unsafe_unretained
-
__autoreleasing
__strong 是默認使用的標識符。只有還有一個強指針指向某個對象,這個對象就會一直存活。
__weak 聲明這個引用不會保持被引用對象的存活,如果對象沒有強引用了,弱引用會被置為 nil
__unsafe_unretained 聲明這個引用不會保持被引用對象的存活,如果對象沒有強引用了,它不會被置為 nil。如果它引用的對象被回收掉了,該指針就變成了野指針。
__autoreleasing 用于標示使用引用傳值的參數(id *),在函數返回時會被自動釋放掉。
變量標識符的用法如下:
Number* __strong num = [[Number alloc] init];
注意 __strong 的位置應該放到 * 和變量名中間,放到其他的位置嚴格意義上說是不正確的,只不過編譯器不會報錯。
屬性標識符
類中的屬性也可以加上標志符:
@property (assign/retain/strong/weak/unsafe_unretained/copy) Number* num
assign 表明 setter 僅僅是一個簡單的賦值操作,通常用于基本的數值類型,例如 CGFloat 和 NSInteger 。
strong 表明屬性定義一個擁有者關系。當給屬性設定一個新值的時候,首先這個值進行 retain ,舊值進行 release ,然后進行賦值操作。
weak 表明屬性定義了一個非擁有者關系。當給屬性設定一個新值的時候,這個值不會進行 retain ,舊值也不會進行 release , 而是進行類似 assign 的操作。不過當屬性指向的對象被銷毀時,該屬性會被置為nil。
unsafe_unretained 的語義和 assign 類似,不過是用于對象類型的,表示一個非擁有(unretained)的,同時也不會在對象被銷毀時置為nil的(unsafe)關系。
copy 類似于 strong ,不過在賦值時進行 copy 操作而不是 retain 操作。通常在需要保留某個不可變對象(NSString最常見),并且防止它被意外改變時使用。
錯誤使用屬性標識符的后果
如果我們給一個原始類型設置 strong\weak\copy ,編譯器會直接報錯:
Property with 'retain (or strong)' attribute must be of object type
設置為 unsafe_unretained 倒是可以通過編譯,只是用起來跟 assign 也沒有什么區別。
反過來,我們給一個 NSObject 屬性設置為 assign,編譯器會報警:
Assigning retained object to unsafe property; object will be released after assignment
正如警告所說的,對象在賦值之后被立即釋放,對應的屬性也就成了野指針,運行時跑到屬性有關操作會直接崩潰掉。和設置成 unsafe_unretained 是一樣的效果(設置成 weak 不會崩潰)。
unsafe_unretained 的用處
unsafe_unretained 差不多是實際使用最少的一個標識符了,在使用中它的用處主要有下面幾點:
-
兼容性考慮。iOS4 以及之前還沒有引入 weak ,這種情況想表達弱引用的語義只能使用 unsafe_unretained 。這種情況現在已經很少見了。
-
性能考慮。使用 weak 對性能有一些影響,因此對性能要求高的地方可以考慮使用 unsafe_unretained 替換 weak 。一個例子是 YYModel 的實現 ,為了追求更高的性能,其中大量使用 unsafe_unretained 作為變量標識符。
引用循環
當兩個對象互相持有對方的強引用,并且這兩個對象的引用計數都不是0的時候,便造成了引用循環。
要想破除引用循環,可以從以下幾點入手:
-
注意變量作用域,使用 autorelease 讓編譯器來處理引用
-
使用弱引用(weak)
-
當實例變量完成工作后,將其置為nil
Autorelease Pool
Autorelase Pool 提供了一種可以允許你向一個對象延遲發送 release 消息的機制。當你想放棄一個對象的所有權,同時又不希望這個對象立即被釋放掉(例如在一個方法中返回一個對象時),Autorelease Pool 的作用就顯現出來了。
所謂的延遲發送 release 消息指的是,當我們把一個對象標記為 autorelease 時:
NSString* str = [[[NSString alloc] initWithString:@"hello"] autorelease];
這個對象的 retainCount 會+1,但是并不會發生 release。當這段語句所處的 autoreleasepool 進行 drain 操作時,所有標記了 autorelease 的對象的 retainCount 會被 -1。即 release 消息的發送被延遲到 pool 釋放的時候了。
在 ARC 環境下,蘋果引入了 @autoreleasepool 語法,不再需要手動調用 autorelease 和 drain 等方法。
Autorelease Pool 的用處
在 ARC 下,我們并不需要手動調用 autorelease 有關的方法,甚至可以完全不知道 autorelease 的存在,就可以正確管理好內存。因為 Cocoa Touch 的 Runloop 中,每個 runloop circle 中系統都自動加入了 Autorelease Pool 的創建和釋放。
當我們需要創建和銷毀大量的對象時,使用手動創建的 autoreleasepool 可以有效的避免內存峰值的出現。因為如果不手動創建的話,外層系統創建的 pool 會在整個 runloop circle 結束之后才進行 drain,手動創建的話,會在 block 結束之后就進行 drain 操作。詳情請參考 蘋果官方文檔 。一個普遍被使用的例子如下:
for (int i = 0; i < 100000000; i++)
{
@autoreleasepool
{
NSString* string = @"ab c";
NSArray* array = [string componentsSeparatedByString:string];
}
}
如果不使用 autoreleasepool ,需要在循環結束之后釋放 100000000 個字符串,如果使用的話,則會在每次循環結束的時候都進行 release 操作。
Autorelease Pool 進行 Drain 的時機
如上面所說,系統在 runloop 中創建的 autoreleaspool 會在 runloop 一個 event 結束時進行釋放操作。我們手動創建的 autoreleasepool 會在 block 執行完成之后進行 drain 操作。需要注意的是:
-
當 block 以異常(exception)結束時,pool 不會被 drain
-
Pool 的 drain 操作會把所有標記為 autorelease 的對象的引用計數減一,但是并不意味著這個對象一定會被釋放掉,我們可以在 autorelease pool 中手動 retain 對象,以延長它的生命周期(在 MRC 中)。
main.m 中 Autorelease Pool 的解釋
大家都知道在 iOS 程序的 main.m 文件中有類似這樣的語句:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在面試中問到有關 autorelease pool 有關的知識也多半會問一下,這里的 pool 有什么作用,能不能去掉之類。在這里我們分析一下。
根據 蘋果官方文檔 , UIApplicationMain 函數是整個 app 的入口,用來創建 application 對象(單例)和 application delegate。盡管這個函數有返回值,但是實際上卻永遠不會返回,當按下 Home 鍵時,app 只是被切換到了后臺狀態。
同時參考蘋果關于 Lifecycle 的 官方文檔 ,UIApplication 自己會創建一個 main run loop,我們大致可以得到下面的結論:
-
main.m 中的 UIApplicationMain 永遠不會返回,只有在系統 kill 掉整個 app 時,系統會把應用占用的內存全部釋放出來。
-
因為(1), UIApplicationMain 永遠不會返回,這里的 autorelease pool 也就永遠不會進入到釋放那個階段
-
在 (2) 的基礎上,假設有些變量真的進入了 main.m 里面這個 pool(沒有被更內層的 pool 捕獲),那么這些變量實際上就是被泄露的。這個 autorelease pool 等于是把這種泄露情況給隱藏起來了。
-
UIApplication 自己會創建 main run loop,在 Cocoa 的 runloop 中實際上也是自動包含 autorelease pool 的,因此 main.m 當中的 pool 可以認為是 沒有 必要的。
在基于 AppKit 框架的 Mac OS 開發中, main.m 當中就是不存在 autorelease pool 的,也進一步驗證了我們得到的結論。不過因為我們看不到更底層的代碼,加上蘋果的文檔中不建議修改 main.m ,所以我們也沒有理由就直接把它刪掉(親測,刪掉之后不影響 App 運行,用 Instruments 也看不到泄露)。
Autorelease Pool 與函數返回值
如果一個函數的返回值是指向一個對象的指針,那么這個對象肯定不能在函數返回之前進行 release,這樣調用者在調用這個函數時得到的就是野指針了,在函數返回之后也不能立刻就 release,因為我們不知道調用者是不是 retain 了這個對象,如果我們直接 release 了,可能導致后面在使用這個對象時它已經成為 nil 了。
為了解決這個糾結的問題, Objective-C 中對對象指針的返回值進行了區分,一種叫做 retained return value ,另一種叫做 unretained return value 。前者表示調用者擁有這個返回值,后者表示調用者不擁有這個返回值,按照“誰擁有誰釋放”的原則,對于前者調用者是要負責釋放的,對于后者就不需要了。
按照蘋果的命名 convention,以 alloc , copy , init , mutableCopy 和 new 這些方法打頭的方法,返回的都是 retained return value,例如 [[NSString alloc] initWithFormat:] ,而其他的則是 unretained return value,例如 [NSString stringWithFormat:] 。我們在編寫代碼時也應該遵守這個 convention。
我們分別在 MRC 和 ARC 情況下,分析一下兩種返回值類型的區別。
MRC
在 MRC 中我們需要關注這兩種函數返回類型的區別,否則可能會導致內存泄露。
對于 retained return value,需要負責釋放
假設我們有一個 property 定義如下:
@property (nonatomic, retain) NSObject *property;
在對其賦值的時候,我們應該使用:
self.property = [[[NSObject alloc] init] autorelease];
然后在 dealloc 方法中加入:
[_property release];
_property = nil;
這樣內存的情況大體是這樣的:
-
init 把 retain count 增加到 1
-
賦值給 self.property ,把 retain count 增加到 2
-
當 runloop circle 結束時,autorelease pool 執行 drain,把 retain count 減為 1
-
當整個對象執行 dealloc 時, release 把 retain count 減為 0,對象被釋放
可以看到沒有內存泄露發生。
如果我們只是使用:
self.property = [[NSObject alloc] init];
這一條語句會導致 retain count 增加到 2,而我們少執行了一次 release,就會導致 retain count 不能被減為 0 。
另外,我們也可以使用臨時變量:
NSObject * a = [[NSObject alloc] init];
self.property = a;
[a release];
這種情況,因為對 a 執行了一次 release,所有不會出現上面那種 retain count 不能減為 0 的情況。
注意:現在大家基本都是 ARC 寫的比較多,會忽略這一點,但是根據上面的內容,我們看到在 MRC 中直接對 self.proprety 賦值和先賦給臨時變量,再賦值給 self.property,確實是有區別的!我在面試中就被問到這一點了。
我們在編寫自己的代碼時,也應該遵守上面的原則,同樣是使用 autorelease:
// 注意函數名的區別
+ (MyCustomClass *) myCustomClass
{
return [[[MyCustomClass alloc] init] autorelease]; // 需要 autorelease
}
- (MyCustomClass *) initWithName:(NSString *) name
{
return [[MyCustomClass alloc] init]; // 不需要 autorelease
}
對于 unretained return value,不需要負責釋放
當我們調用非 alloc,init 系的方法來初始化對象時(通常是工廠方法),我們不需要負責變量的釋放,可以當成普通的臨時變量來使用:
NSString *name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
self.name = name
// 不需要執行 [name release]
ARC
在 ARC 中我們完全不需要考慮這兩種返回值類型的區別,ARC 會自動加入必要的代碼,因此我們可以放心大膽地寫:
self.property = [[NSObject alloc] init];
self.name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
以及在自己寫的函數中:
+ (MyCustomClass *) myCustomClass
{
return [[MyCustomClass alloc] init]; // 不用 autorelease
}
這些寫法都是 OK 的,也不會出現內存問題。
為了進一步理解 ARC 是如何做到這一點的,我們可以參考 Clang 的 文檔 。
對于 retained return value, Clang 是這樣做的:
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, before leaving all local scopes.
When receiving a return result from such a function or method, ARC releases the value at the end of the full-expression it is contained within, subject to the usual optimizations for local values.
可以看到基本上 ARC 就是幫我們在代碼塊結束的時候進行了 release:
NSObject * a = [[NSObject alloc] init];
self.property = a;
//[a release]; 我們不需要寫這一句,因為 ARC 會幫我們把這一句加上
對于 unretained return value:
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.
ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.
這個和我們之前在 MRC 中做的不是完全一樣。ARC 會把對象的生命周期延長,確保調用者能拿到并且使用這個返回值,但是并不一定會使用 autorelease,文檔寫的是在 worst case 的情況下才可能會使用,因此調用者不能假設返回值真的就在 autorelease pool 中。從性能的角度,這種做法也是可以理解的。如果我們能夠知道一個對象的生命周期最長應該有多長,也就沒有必要使用 autorelease 了,直接使用 release 就可以。如果很多對象都使用 autorelease 的話,也會導致整個 pool 在 drain 的時候性能下降。
weak 與 autorelease
眾所周知,weak 不會持有對象,當給一個 weak 賦以一個自己生成的對象(即上面提到的 retained return value)后,對象會立馬被釋放。
一個很常見的 warning 就是 Assigning retained object to weak variable, object will be released after assignment.
但是我們前面也提到了,可以持有非自己生成的對象,這通過 autorelease 實現。
那么如果一個 weak 被賦以一個非自己生成的對象(即上面提到的 unretained return value)呢?代碼如下:
NSNumber __weak *number = [NSNumber numberWithInt:100];
NSLog(@"number = %@", number);
這種情況下是可以正確打印值的。
clang的文檔 是這么說的:這種情況下,weak 并不會立即釋放,而是會通過 objc_loadWeak 這個方法注冊到 AutoreleasePool 中,以延長生命周期。
ARC 下是否還有必要在 dealloc 中把屬性置為 nil?
為了解決這個問題,首先讓我們理清楚屬性是個什么存在。屬性(property) 實際上就是一種語法糖,每個屬性背后都有實例變量(Ivar)做支持,編譯器會幫我們自動生成有關的 setter 和 getter,對于下面的 property:
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;
生成的 getter 和 setter 類似下面這樣:
- (NSNumber *)count {
return _count;
}
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// Make the new assignment.
_count = newCount;
}
Property 這部分對于 MRC 和 ARC 都是適用的。
有了這部分基礎,我們再來理解一下把屬性置為 nil 這個步驟。首先要明確一點,在 MRC 下,我們并不是真的把屬性置為 nil,而是把 Ivar 置為 nil。
[_property release];
_property = nil;
如果用 self.property 的話還會調用 setter,里面可能存在某些不應該在 dealloc 時運行的代碼。
對于 ARC 來說,系統會自動在 dealloc 的時候把所有的 Ivar 都執行 release,因此我們也就沒有必要在 dealloc 中寫有關 release 的代碼了。
在 ARC 下把變量置為 nil 有什么效果?什么情況下需要把變量置為 nil?
在上面有關 property 的內容基礎上,我們知道用:
self.property = nil
實際上就是手動執行了一次 release。而對于臨時變量來說:
NSObject *object = [[NSObject alloc] init];
object = nil;
置為 nil 這一句其實沒什么用(除了讓 object 在下面的代碼里不能再使用之外),因為上面我們討論過 ,ARC 下的臨時變量是受到 Autorelease Pool 的管理的,會自動釋放。
因為 ARC 下我們不能再使用 release 函數,把變量置為 nil 就成為了一種釋放變量的方法。真正需要我們把變量置為 nil 的,通常就是在使用 block 時,用于破除循環引用:
MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler = ^(NSInteger result) {
[myController dismissViewControllerAnimated:YES completion:nil];
myController = nil;
};
在 YTKNetwork 這個項目中,也可以看到類似的代碼:
- (void)clearCompletionBlock {
// nil out to break the retain cycle.
self.successCompletionBlock = nil;
self.failureCompletionBlock = nil;
}
ARC 在運行時期的優化
上面提到對于 unretained return value, ARC “并不一定會使用 autorelease”,下面具體解釋一下。
ARC 所做的事情并不僅僅局限于在編譯期找到合適的位置幫你插入合適的 release 等等這樣的內存管理方法,其在運行時期也做了一些優化,如下是兩個優化的例子:
-
合并對稱的引用計數操作。比如將 +1/-1/+1/-1 直接置為 0.
-
巧妙地跳過某些情況下 autorelease 機制的調用。
其中第二個優化,是 ARC 針對 autorelease 返回值提供的一套優化策略,大體的流程如下:
當方法全部基于 ARC 實現時,在方法 return 的時候,ARC 會調用 objc_autoreleaseReturnValue() 以替代 MRC 下的 autorelease 。在 MRC 下需要 retain 的位置,ARC 會調用 objc_retainAutoreleasedReturnValue() 。因此下面的 ARC 代碼:
+ (instancetype)createSark {
return [self new];
}
// caller
Sark *sark = [Sark createSark];
實際上會被改寫成類似這樣:
+ (instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我們調用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相當于代替我們調用了release
有了這個基礎,ARC 可以使用一些優化技術。在調用 objc_autoreleaseReturnValue() 時,會在棧上查詢 return address 以確定 return value 是否會被直接傳給 objc_retainAutoreleasedReturnValue() 。 如果沒傳,說明返回值不能直接從提供方發送給接收方,這時就會調用 autorelease 。反之,如果返回值能順利的從提供方傳送給接收方,那么就會直接跳過 autorelease 過程,并且修改 return address 以跳過 objc_retainAutoreleasedReturnValue() 過程,這樣就跳過了整個 autorelease 和 retain 的過程。
核心思想:當返回值被返回之后,緊接著就需要被 retain 的時候,沒有必要進行 autorelease + retain,直接什么都不要做就好了。
另外,當函數的調用方是非 ARC 環境時,ARC 還會進行更多的判斷,在這里不再詳述,詳見 《黑幕背后的 Autorelease》 。
關于如何寫一個檢測循環引用的工具
Instrument 為我們提供了 Allocations/Leaks 這樣好用的工具用來檢測 memory leak 的工具。如下是內存泄露的兩種類型:
-
Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
-
Abandoned memory: Memory still referenced by your application that has no useful purpose.
其中 Leaks 工具主要用來檢測 Leaked memory,在 MRC 時代 程序員會經常忘記寫 release 方法導致內存泄露,在 ARC 時代這種已經不太常見。(ARC時代 主要的Leaked Memory 來自于底層 C 語言以及 一些由 C 寫成的底層庫,往往會因為忘記手工 free 而導致 leak )。
Allocations 工具主要用來檢測 Abandoned memory. 主要思路是在一個時間切片內檢測對象的聲明周期以觀察內存是否會無限增長。通過 hook 掉 alloc,dealloc,retain,release 等方法,來記錄對象的生命周期。