有關 iOS 內存管理的文章相信你們都看過很多了,我本身也同樣。不過網上大部分文章都沒有解決對於內存管理的一些關鍵性的疑惑,對於初學者來講並非很友好。本文旨在從初學者的角度出發,對 iOS 內存管理的關鍵部分進行介紹,但願能對廣大 iOS 學習者有所幫助。html
本文首發於我發起的 筆試面試知識整理 項目,這裏是對應的 Github 倉庫,由多人協做編輯而成。如發現有錯誤的地方,隨時歡迎指正。ios
在 Objective-C 中,對象一般是使用 alloc
方法在堆上建立的。 [NSObject alloc]
方法會在對堆上分配一塊內存,按照NSObject
的內部結構填充這塊兒內存區域。git
一旦對象建立完成,就不可能再移動它了。由於極可能有不少指針都指向這個對象,這些指針並無被追蹤。所以沒有辦法在移動對象的位置以後更新所有的這些指針。程序員
Objective-C中提供了兩種內存管理機制:MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分別提供對內存的手動和自動管理,來知足不一樣的需求。如今蘋果推薦使用 ARC 來進行內存管理。github
對象操做 | OC中對應的方法 | 對應的 retainCount 變化 |
---|---|---|
生成並持有對象 | alloc/new/copy/mutableCopy等 | +1 |
持有對象 | retain | +1 |
釋放對象 | release | -1 |
廢棄對象 | dealloc | - |
注意:面試
這些對象操做的方法其實並不包括在OC中,而是包含在Cocoa框架下的Foundation框架中。objective-c
對象的 reatinCount
屬性並無實際上的參考價值,參考蘋果官方文檔《Practical Memory Management》.express
本身生成的對象,本身持有。app
非本身生成的對象,本身也能持有。框架
不在須要本身持有的對象的時候,釋放。
非本身持有的對象沒法釋放。
以下是四個黃金法則對應的代碼示例:
/* * 本身生成並持有該對象 */ 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 是蘋果引入的一種自動內存管理機制,會根據引用計數自動監視對象的生存週期,實現方式是在編譯時期自動在已有代碼中插入合適的內存管理代碼以及在 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
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
等方法。
在 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 操做。
如上面所說,系統在 runloop 中建立的 autoreleaspool 會在 runloop 一個 event 結束時進行釋放操做。咱們手動建立的 autoreleasepool 會在 block 執行完成以後進行 drain 操做。須要注意的是:
當 block 以異常(exception)結束時,pool 不會被 drain
Pool 的 drain 操做會把全部標記爲 autorelease 的對象的引用計數減一,可是並不意味着這個對象必定會被釋放掉,咱們能夠在 autorelease pool 中手動 retain 對象,以延長它的生命週期(在 MRC 中)。
你們都知道在 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 也看不到泄露)。
若是一個函數的返回值是指向一個對象的指針,那麼這個對象確定不能在函數返回以前進行 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 中咱們須要關注這兩種函數返回類型的區別,不然可能會致使內存泄露。
對於 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 會自動加入必要的代碼,所以咱們能夠放心大膽地寫:
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 不會持有對象,當給一個 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 中,以延長生命週期。
爲了解決這個問題,首先讓咱們理清楚屬性是個什麼存在。屬性(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 的代碼了。
在上面有關 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; }
上面提到對於 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 等方法,來記錄對象的生命週期。
https://stackoverflow.com/questions/9784762/strong-weak-retain-unsafe-unretained-assign
https://stackoverflow.com/questions/29350634/ios-autoreleasepool-in-main-and-arc-alloc-release
https://stackoverflow.com/questions/6588211/why-do-the-ios-main-m-templates-include-a-return-statement-and-an-autorelease-po
https://stackoverflow.com/questions/2702548/if-the-uiapplicationmain-never-returns-then-when-does-the-autorelease-pool-get
https://stackoverflow.com/questions/6055274/use-autorelease-when-setting-a-retain-property-using-dot-syntax
https://stackoverflow.com/questions/17601274/arc-and-autorelease
https://stackoverflow.com/questions/8292060/arc-equivalent-of-autorelease
https://stackoverflow.com/questions/7906804/do-i-set-properties-to-nil-in-dealloc-when-using-arc
http://wereadteam.github.io/2016/02/22/MLeaksFinder/?from=singlemessage&isappinstalled=0
http://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-loadweak