iOS開發中,內存管理是不可避免的。鑑於當下MRC已經遠去多時,本篇學習筆記主要針對ARC下的內存管理進行實踐。程序員
內存理論篇:編程
內存理論實踐篇:數組
緣由:bash
- 通常繼承了 NSObject 的對象,存儲在操做系統到 堆 裏邊。(PS:並不是全部都是這樣,建立的字符串有時候根據建立方式、位置,也會存儲到 常量區)
- 操做系統的 堆:通常由程序員分配釋放,若程序員不釋放,結束時可能由系統回收,分配方式相似數據結構的鏈表。
- 操做系統的 棧:由操做系統自動分配釋放內存,存放函數的參數值、局部變量值等。其操做方式相似數據結構中的 棧(先進後出)。
示例:數據結構
int main(int argc, const char *argv []) {
@autoreleasepool {
int a = 10; // 棧
int b = 20; // 棧
// p: 棧
// Person 對象(計數器 == 1):堆
Person *p = [[Person alloc] init];
}
// 通過上面代碼後,棧裏的變量 a、b、p 都會被回收
// 可是堆裏的 Person 對象仍會留在內存中,由於它的計數器依然是 1
return 0;
}
複製代碼
block內存分爲三種類型:app
- 一、_NSConcreteGlobalBlock(全局)
當咱們聲明一個block時,若是這個block沒有捕獲外部的變量,那麼這個block就位於全局區,此時對_NSConcreteGlobalBlock的retain、copy、release操做都無效。ARC和MRC環境下都是如此。框架
示例:聲明並定義一個全局區block函數
void (^myBlock) (int x); myBlock = ^(int number) { int result = number + 100; NSLog(@"result: %d",result); }; myBlock(10); 複製代碼
- 二、_NSConcreteStackBlock(棧)
棧區block咱們平時編程基本不會遇到!由於在ARC環境下,當咱們聲明而且定義了一個block,而且沒有爲Block添加額外的修飾符(默認是__strong修飾符),若是該Block捕獲了外部的變量,實質上是有一個從_NSConcreteStackBlock轉變到_NSConcreteMallocBlock的過程,只不過是系統幫咱們完成了copy操做,將棧區的block遷移到堆區,延長了Block的生命週期。對於棧區block而言,棧block在當函數退出的時候,該空間就會被回收。oop
那何時在ARC的環境下出現_NSConcreteStackBlock呢?若是咱們在聲明一個block的時候,使用了__weak或者__unsafe__unretained的修飾符,那麼系統就不會爲咱們作copy的操做,不會將其遷移到堆區。下面咱們實驗一下:post
__weak void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"myBlock1: %@",myBlock1); //打印結果 "myBlock1:<__NSStackBlock__:0x7ffff50726c0>" //結論:被__weak修飾的myBlock1捕獲了外部變量n,成爲一個棧區的block 複製代碼
void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"默認myBlock1: %@",myBlock1); //打印結果 "默認myBlock1:<__NSMallocBlock__:0x604000259020>" //結論:不使用__weak修飾,在默認修飾符環境下,捕獲了外部變量的block位於堆區 複製代碼
咱們能夠手動地去執行copy方法,驗證系統爲咱們作的隱式轉換:
__weak void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"手動copy myBlock1: %@",[myBlock1 copy]); //打印結果 "手動copy myBlock1:<__NSMallocBlock__:0x60000025c020>" //結論:手動執行copy方法以後,block被遷移到了堆區 複製代碼
- 三、_NSConcreteMallocBlock(堆)
在MRC環境下,咱們須要手動調用copy方法才能夠將block遷移到堆區,而在ARC環境下,__strong修飾的(默認)block只要捕獲了外部變量就會位於堆區,NSMallocBlock支持retain、release,會對其引用計數+1或 -1。
- 一、當使用局部變量時,須要添加__block
__blockintnum =100; self.tBlock= ^(int n) { num = num + n; NSLog(@"%d",num); }; self.tBlock(100); 複製代碼
- 二、當使用全局變量時,須要時使用__weak typeof(self)weakSelf = self修飾,不然會形成循環引用
__weak typeof(self)weakSelf = self; weakSelf.qBlock= ^(NSString*str) { NSLog(@"%@",weakSelf.nameStr); }; 複製代碼
- 三、 這樣循環問題是解決了,可是又會致使一個新的問題,假如在block有一個耗時操做,在這個過程self被銷燬了,而weakself也會隨着self的銷燬而銷燬,block又要對weakself進行某些操做,這是拿到的weakself就是nil了。(緣由請參考iOS-內存管理-理論篇 __weak -內存理論)
__weak typeof(self)weakSelf = self; weakSelf.qBlock= ^(NSString*str) { _strong__typeof(self) strongSelf = weakSelf; NSLog(@"%@",strongSelf.nameStr); }; self.tBlock(100); 複製代碼
block做爲屬性,使用copy修飾時(strong修飾符不會改變block內存類型),所以使用copy或strong修飾均可以。block中使用weak通常是爲了防止循環引用,爲了不重複,在這裏就不過多介紹weak的使用。
項目當中使用block儘可能不要嵌套,若是實在嵌套也請控制在一層。否則很容易形成內存泄露或是地獄回調。特別是若是是用block進行數據傳遞,多層嵌套的block很容易形成數據缺失,app崩潰,並且項目複雜之後很難排查。
Runtime維護了一個weak表,用於存儲指向某個對象的全部weak指針,對於 weak 對象會放入一個 hash 表中,Key是所指對象的地址,Value是weak指針的地址(這個地址的值是所指對象的地址)數組。 當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。
注:因爲可能多個weak指針指向同一個對象,因此value爲一個數組
weak 的實現原理能夠歸納如下三步:
- 一、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。 示例代碼:
{ id __weak obj1 = obj; } 複製代碼
當咱們初始化一個weak變量時,runtime會調用objc_initWeak函數。這個函數在Clang中的聲明以下:
id objc_initWeak(id *object, id value); 複製代碼
其具體實現以下:
id objc_initWeak(id *object, id value) { *object = 0; return objc_storeWeak(object, value); } 複製代碼
示例代碼輪換成編譯器的模擬代碼以下:
id obj1; objc_initWeak(&obj1, obj); 複製代碼
所以,這裏所作的事是先將obj1初始化爲0(nil),而後將obj1的地址及obj做爲參數傳遞給objc_storeWeak函數。objc_initWeak函數有一個前提條件:就是object必須是一個沒有被註冊爲__weak對象的有效指針。而value則能夠是null,也能夠指向一個有效的對象。
- 二、添加引用時:objc_initWeak函數會調用 objc_storeWeak() 函數。
objc_storeWeak() 的做用是更新指針指向,建立對應的弱引用表。
- 三、釋放時,調用clearDeallocating函數。
clearDeallocating函數首先根據對象地址獲取全部weak指針地址的數組,而後遍歷這個數組把其中的數據設爲nil,最後把這個entry從weak表中刪除,最後清理對象的記錄。
在dealloc的時候,會將weak屬性的值會自動設置爲nil
- 一、系統經過runloop建立的autoreleasePool
runloop 能夠說是iOS 系統的靈魂。內存管理/UI 刷新/觸摸事件這些功能都須要 runloop 去管理和實現。runloop是經過線程建立的,和線程保持一對一的關係,其關係是保存在一個全局的 Dictionary 裏。線程剛建立時並無 RunLoop,若是你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。
runloop和autoreleasePool又是什麼關係呢?對象又是何時釋放的?
App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後。
在主線程執行的代碼,一般是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯示建立 Pool 了。
- 二、手動autoreleasePool
咱們能夠經過@autoreleasepool {}方式手動建立autoreleasepool對象,那麼這個對象何時釋放呢?答案是除了autoreleasepool的大括號就釋放了。
- 三、子線程的autoreleasepool對象的管理?
線程剛建立時並無 RunLoop,若是你不主動獲取,那它一直都不會有。因此在咱們建立子線程的時候,若是沒有獲取runloop,那麼也就沒用經過runloop來建立autoreleasepool,那麼咱們的autorelease對象是怎麼管理的,會不會存在內存泄漏呢?答案是否認的,當子線程有autoreleasepool的時候,autorelease對象經過其來管理,若是沒有autoreleasepool,會經過調用 autoreleaseNoPage 方法,將對象添加到 AutoreleasePoolPage 的棧中,也就是說你不進行手動的內存管理,也不會內存泄漏啦!這部分咱們能夠看下runtime中NSObject.mm的部分,有相關代碼。
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { //調用 autoreleaseNoPage 方法管理autorelease對象。 return autoreleaseNoPage(obj); } } 複製代碼
- 一、使用autorelease有什麼好處呢?
- 不在關心對象的釋放時間
- 不在關心何時調用 release
- 二、autorelease 的建立方法
- 使用 NSAutoreleasePool 來建立:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 建立自動釋放池 [pool release]; // [pool drain]; 銷燬自動釋放池 複製代碼
- 使用 @autoreleasepool 建立
@autoreleasepool { //開始表明建立自動釋放池 } //結束表明銷燬自動釋放池 複製代碼
- 三、autorelease 的使用方法
- NSAutoreleasePool 用法:
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; Person *p = [[[Person alloc] init] autorelease]; [autoreleasePool drain]; 複製代碼
- @autoreleasepool 用法:
@autoreleasepool { // 建立一個自動釋放池 Person *p = [[Person new] autorelease]; // 將代碼寫到這裏就放入了自動釋放池 } // 銷燬自動釋放池(會給池子中全部對象發送一條release消息) 複製代碼
- 四、autorelease 的注意事項
- 並非放到自動釋放池代碼中,就會自動加入自動釋放池
錯誤案例1 @autoreleasepool { // 由於沒有調用 autorelease 方法,因此對象沒有加入到自動釋放池 Person *p = [[Person alloc] init]; [p run]; } 複製代碼
- 在自動釋放池的外部調用 autorelease 不會被加入到自動釋放池中。autorelease 是一個方法,只有在自動釋放池中調用纔有效
錯誤案例2 @autoreleasepool { } // 沒有與之對應的自動釋放池, 只有在自動釋放池中調用autorelease纔會放到釋放池 Person *p = [[[Person alloc] init] autorelease]; [p run]; // 正確案例1 @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } // 正確案例2 Person *p = [[Person alloc] init]; @autoreleasepool { [p autorelease]; } 複製代碼
- 五、autorelease 經典錯誤案例實際當中容易犯的錯誤
自動釋放池內不宜放佔用內存比較大的對象
- 儘可能避免對大內存使用該方法,對這種延遲釋放機制,仍是儘可能少用。
- 不要把大量循環操做放到一個 autoreleasepool 之間,這樣會形成內存峯值的上升
// 內存暴漲 @autoreleasepool { for (int i = 0; i < 99999; ++i) { //若是Person對象內存佔用大這種寫法在少許循環中就會形成嚴重內存泄露 Person *p = [[[Person alloc] init] autorelease]; } } // 內存不會暴漲 for (int i = 0; i < 99999; ++i) { @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } } 複製代碼