iOS-內存管理-理論實踐1

前言

iOS開發中,內存管理是不可避免的。鑑於當下MRC已經遠去多時,本篇學習筆記主要針對ARC下的內存管理進行實踐。程序員

內存理論篇:編程

內存理論實踐篇:數組

實踐哪些對象須要咱們進行內存管理呢?

  • 項目中全部實例對象原則上都須要進行內存管;繼承了 NSObject 的對象,能夠經過ARC由系統進行管理;非NSObject對象,須要本身進行對應內存管理。
  • 而其餘非對象類型,如 int(NSInteger) 、 char 、 float(CGFloat) 、 double 、 struct 、 enum 等,不須要進行內存管理。

緣由: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內存管理
  • weak等防止循環引用的內存管理
  • autorelease內存管理
  • 非OC對象,例如CF框架,C/C++語言混編等內存管理(待續)
  • 計時器、通知等內存管理(待續)
  • 屬性的內存管理(待續)

1、block內存管理

一、block內存類型

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實際運用

  • 一、當使用局部變量時,須要添加__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 小結:

block做爲屬性,使用copy修飾時(strong修飾符不會改變block內存類型),所以使用copy或strong修飾均可以。block中使用weak通常是爲了防止循環引用,爲了不重複,在這裏就不過多介紹weak的使用。

項目當中使用block儘可能不要嵌套,若是實在嵌套也請控制在一層。否則很容易形成內存泄露或是地獄回調。特別是若是是用block進行數據傳遞,多層嵌套的block很容易形成數據缺失,app崩潰,並且項目複雜之後很難排查。

2、weak等防止循環引用的內存管理

一、weak的實現原理

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

3、autorelease內存管理

一、autoreleasePool何時建立的,裏面的對象又是何時釋放的?

  • 一、系統經過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在實際當中的應用?

  • 一、使用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];
    }
}
複製代碼
相關文章
相關標籤/搜索