iOS-內存管理-理論篇

前言

關於iOS的內存管理網上已有不少前輩大神提供了不少學習筆記和博客,很是感謝可以從他們那裏學到東西,此篇只是根據自身的總結學習。算法

內存基本知識

  • 1.內存的基本劃分:

    1.棧區(heap):由系統去管理。地址從高到低分配。先進後出。會存一些局部變量,函數跳轉跳轉時現場保護(寄存器值保存於恢復),這些系統都會幫咱們自動實現,無需咱們干預。因此大量的局部變量,深遞歸,函數循環調用均可能耗盡棧內存而形成程序崩潰 。編程

    2.堆區(stack):須要咱們本身管理內存,alloc申請內存release釋放內存。建立的對象也都放在這裏。 地址是從低到高分配。堆是全部程序共享的內存,當N個這樣的內存得不到釋放,堆區會被擠爆,程序立馬癱瘓。這就是內存泄漏。安全

    3.全局區/靜態區(staic):全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。程序結束後有系統釋放。bash

    4.常量區:常量字符串就是放在這裏的,還有const常量。編程語言

    5.代碼區:存放App代碼,App程序會拷貝到這裏。 函數

    簡單總結 在iOS中數據是存在在堆和棧中的,然而咱們的內存管理管理的是堆上的內存,棧上的內存並不須要咱們管理。工具

    • 非OC對象(基礎數據類型)存儲在棧上
    • OC對象存儲在堆上
  • 2.引用計數

    引用計數是計算機編程語言中的一種內存管理技術,是指將資源(能夠是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變爲零時就將其釋放的過程。使用引用計數技術能夠實現自動資源管理的目的。同時引用計數還能夠指使用引用計數技術回收未使用資源的垃圾回收算法。性能

    當建立一個對象的實例並在堆上申請內存時,對象的引用計數就爲1,在其餘對象中須要持有這個對象時,就須要把該對象的引用計數加1,須要釋放一個對象時,就將該對象的引用計數減1,直至對象的引用計數爲0,對象的內存會被馬上釋放。學習

    在遙遠的之前,iOS開發的內存管理是手動處理引用計數,在合適的地方使引用計數-1,直到減爲0,內存釋放。如今的iOS開發內存管理使用的是ARC,自動管理引用計數,會根據引用計數自動監視對象的生存週期,實現方式是在編譯時期自動在已有代碼中插入合適的內存管理代碼以及在 Runtime 作一些優化。優化

內存管理基本知識

  • 一、ARC自動管理內存(Automatic Reference Counting)

    Automatic Reference Counting,自動引用計數,即 ARC,WWDC2011 和 iOS5 所引用的最大變革和最激動人心的變化。ARC 是新的 LLVM3.0 編譯器的一項特性,使用 ARC,能夠說是一舉解決了廣大 iOS 開發者所憎惡的手動內存管理的麻煩。使用 ARC 後,系統會檢測出什麼時候須要保持對象,什麼時候須要自動釋放對象,什麼時候須要釋放對象,編譯器會管理好對象的內存,會在合適的地方插入retain 、 release 、 autorelease ,經過生成正確的代碼,去自動釋放或者保持對象。

    全部權修飾符: Objective-C編程中爲了處理對象,可將變量類型定義爲id類型或各類對象類型。 ARC中id類型和對象類其類型必須附加全部權修飾符。

    其中有如下4種全部權修飾符:

    • __strong
    • __weak
    • __unsafe_unretaied
    • __autoreleasing

    全部權修飾符和屬性的修飾符對應關係以下所示:

    • assign 對應的全部權類型是 __unsafe_unretained
    • copy 對應的全部權類型是 __strong
    • retain 對應的全部權類型是 __strong
    • strong 對應的全部權類型是 __strong
    • unsafe_unretained對應的全部權類型是__unsafe_unretained
    • weak 對應的全部權類型是 __weak

    __strong

    __strong 表示強引用,對應定義 property 時用到的 strong。當對象沒有任何一個強引用指向它時,它纔會被釋放。若是在聲明引用時不加修飾符,那麼引用將默認是強引用。當須要釋放強引用指向的對象時,須要保證全部指向對象強引用置爲 nil。__strong 修飾符是 id 類型和對象類型默認的全部權修飾符。

    原理:

    一、對象是經過alloc方法產生的狀況

    {
     id __strong obj = [[NSObject alloc] init];
    }
    複製代碼
    //編譯器的模擬代碼
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    // 出做用域的時候調用
    objc_release(obj);
    複製代碼

    雖然ARC有效時不能使用release方法,但由此可知編譯器自動插入了release。

    二、對象是經過除alloc、new、copy、multyCopy外方法產生的狀況

    {
     id __strong obj = [NSMutableArray array];
    }
    複製代碼

    結果與以前稍有不一樣:

    //編譯器的模擬代碼
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);
    複製代碼

    objc_retainAutoreleasedReturnValue函數主要用於優化程序的運行。它是用於持有(retain)對象的函數,它持有的對象應爲返回註冊在autoreleasePool中對象的方法,或是函數的返回值。像該源碼這樣,在調用array類方法以後,由編譯器插入該函數。

    這種objc_retainAutoreleasedReturnValue函數是成對存在的,與之對應的函數是objc_autoreleaseReturnValue。它用於array類方法返回對象的實現上。下面看看NSMutableArray類的array方法經過編譯器進行了怎樣的轉換:

    {
     id __strong obj = [[NSObject alloc] init];
    }
    複製代碼
    //編譯器的模擬代碼
    + (id)array
    {
       id obj = objc_msgSend(NSMutableArray,@selector(alloc));
       objc_msgSend(obj,@selector(init));
       // 代替咱們調用了autorelease方法
       return objc_autoreleaseReturnValue(obj);
    }
    複製代碼

    咱們能夠看見調用了objc_autoreleaseReturnValue函數且這個函數會返回註冊到自動釋放池的對象,可是,這個函數有個特色,它會查看調用方的命令執行列表,若是發現接下來會調用objc_retainAutoreleasedReturnValue則不會將返回的對象註冊到autoreleasePool中而僅僅返回一個對象。達到了一種最優效果。以下圖:

    __weak

    __weak 表示弱引用,對應定義 property 時用到的 weak。弱引用不會影響對象的釋放,而當對象被釋放時,全部指向它的弱引用都會自定被置爲 nil,這樣能夠防止野指針。使用__weak修飾的變量,便是使用註冊到autoreleasePool中的對象。__weak 最多見的一個做用就是用來避免循環循環。須要注意的是,__weak 修飾符只能用於 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修飾符來代替。

    __weak 的幾個使用場景:

    • 在 Delegate 關係中防止循環引用。
    • 在 Block 中防止循環引用。
    • 用來修飾指向由 Interface Builder 建立的控件。好比:@property (weak, nonatomic) IBOutlet UIButton *testButton;。

    原理:

    一、對象是經過alloc方法產生的狀況

    {
     id __weak obj = [[NSObject alloc] init];
    }
    複製代碼
    //編譯器轉換後的模擬代碼
    id obj;
    id tmp = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(tmp,@selector(init));
    objc_initweak(&obj,tmp);
    objc_release(tmp);
    objc_destroyWeak(&object);
    複製代碼

    對於__weak內存管理也藉助了相似於引用計數表的散列表,它經過對象的內存地址作爲key,而對應的__weak修飾符變量的地址做爲value註冊到weak表中,在上述代碼中objc_initweak就是完成這部分操做,而objc_destroyWeak則是銷燬該對象對應的value。當指向的對象被銷燬時,會經過其內存地址,去weak表中查找對應的__weak修飾符變量,將其從weak表中刪除。因此,weak在修飾只是讓weak表增長了記錄沒有引發引用計數表的變化。

    對象經過objc_release釋放對象內存的動做以下:

    • objc_release
    • 由於引用計數爲0因此執行dealloc
    • _objc_rootDealloc
    • objc_dispose
    • objc_destructInstance
    • objc_clear_deallocating

    而在對象被廢棄時最後調用了objc_clear_deallocating,該函數的動做以下:

    對象經過objc_release釋放對象內存的動做以下:

    • 從weak表中獲取已廢棄對象內存地址對應的全部記錄
    • 將已廢棄對象內存地址對應的記錄中全部以weak修飾的變量都置爲nil
    • 從weak表刪除已廢棄對象內存地址對應的記錄
    • 根據已廢棄對象內存地址從引用計數表中找到對應記錄刪除
    • 據此能夠解釋爲何對象被銷燬時對應的weak指針變量所有都置爲nil,同時,也看出來銷燬weak步驟較多,若是大量使用weak的話會增長CPU的負荷

    還須要確認一點是:使用__weak修飾符的變量,便是使用註冊到autoreleasePool中的對象。

    {
       id __weak obj1 = obj; 
       NSLog(@"obj2-%@",obj1);
    }
    複製代碼
    //編譯器轉換上述代碼以下:
    id obj1; 
    objc_initweak(&obj1,obj);
    id tmp = objc_loadWeakRetained(&obj1);
    objc_autorelease(tmp);
    NSLog(@"%@",tmp);
    objc_destroyWeak(&obj1);
    複製代碼

    objc_loadWeakRetained函數獲取附有__weak修飾符變量所引用的對象並retain, objc_autorelease函數將對象放入autoreleasePool中,據此當咱們訪問weak修飾指針指向的對象時,其實是訪問註冊到自動釋放池的對象。所以,若是大量使用weak的話,在咱們去訪問weak修飾的對象時,會有大量對象註冊到自動釋放池,這會影響程序的性能,同時也可能對象已經被釋放。

    爲何訪問weak修飾的對象就會訪問註冊到自動釋放池的對象呢?

    由於weak不會引發對象的引用計數器變化,所以,該對象在運行過程當中頗有可能會被釋放。因此,須要將對象註冊到自動釋放池中並在autoreleasePool銷燬時釋放對象佔用的內存。

    解決方案:要訪問weak修飾的變量時,先將其賦給一個strong變量,而後進行訪問。

    __unsafe_unretained

    ARC 是在 iOS5 引入的,而 __unsafe_unretained 這個修飾符主要是爲了在ARC剛發佈時兼容iOS4以及版本更低的系統,由於這些版本沒有弱引用機制。這個修飾符在定義property時對應的是unsafe_unretained。__unsafe_unretained 修飾的指針純粹只是指向對象,沒有任何額外的操做,不會去持有對象使得對象的 retainCount +1。而在指向的對象被釋放時依然原本來本地指向原來的對象地址,不會被自動置爲 nil,因此成爲了野指針,很是不安全。

    __unsafe_unretained的應用場景: 在 ARC 環境下可是要兼容 iOS4.x 的版本,用__unsafe_unretained 替代 __weak 解決強循環循環的問題。

    __autoreleasing

    將對象賦值給附有__autoreleasing修飾符的變量等同於MRC時調用對象的autorelease方法。

    @autoeleasepool {
       // 若是看了上面__strong的原理,就知道實際上對象已經註冊到自動釋放池裏面了
       id __autoreleasing obj = [[NSObject alloc] init];
    }
    複製代碼
    //編譯器轉換上述代碼以下1:
    id pool = objc_autoreleasePoolPush();  
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);
    @autoreleasepool {
       id __autoreleasing obj = [NSMutableArray array];
    }
    複製代碼
    //編譯器轉換上述代碼以下2:
    id pool = objc_autoreleasePoolPush();  
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);
    複製代碼

    上面兩種方式,雖然第二種持有對象的方法從alloc方法變爲了objc_retainAutoreleasedReturnValue函數,但同樣都是經過objc_autorelease,註冊到autoreleasePool中。

  • 二、MRC 手動管理內存(Manual Reference Counting)-簡單介紹

    在MRC中增長的引用計數都是須要本身手動釋放的,因此咱們須要知道哪些方式會引發引用計數+1

    對象操做 OC中對應的方法 引用計數的變化
    生成並持有對象 alloc/new/copy/mutableCopy +1
    持有對象 retain +1
    釋放對象 release -1
    廢棄對象 dealloc 歸0

    四個法則

    • 本身生成的對象,本身持有
    • 非本身生成的對象,本身也能持有。
    • 不在須要本身持有對象的時候,釋放。
    • 非本身持有的對象無需釋放。
  • 三、循環引用

    什麼是循環引用?循環引用就是在兩個對象互相之間強引用了,引用計數都加1了,咱們前面說過,只有當引用計數減爲0時對象才釋放。可是這兩個的引用計數都依賴於對方,因此也就致使了永遠沒法釋放

    最容易產生循環引用的兩種狀況就是Delegate和Block。因此咱們就引入了弱引用這種概念,即弱引用雖然持有對象,可是並不增長引用計數,這樣就避免了循環引用的產生。也就是咱們上面所說的全部權修飾符__weak的做用。關於原理在__weak部分也有描述,簡單的描述就是每個擁有弱引用的對象都有一張表來保存弱引用的指針地址,可是這個弱引用並不會使對象引用計數加1,因此當這個對象的引用計數變爲0時,系統就經過這張表,找到全部的弱引用指針把它們都置成nil

    因此在ARC中作內存管理主要就是發現這些內存泄漏,關於內存泄漏Instrument爲咱們提供了 Allocations/Leaks 這樣的工具用來檢測。

    相似內存泄漏工具MLeaksFinder的原理,具體原理以下:

    咱們知道,當一個 UIViewController 被 pop 或 dismiss 後,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設計成單例,或者持有它的強引用,但通常不多這樣作)。因而,咱們只需在一個 ViewController 被 pop 或 dismiss 一小段時間後,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在。

    具體的方法是,爲基類 NSObject 添加一個方法 -willDealloc 方法,該方法的做用是,先用一個弱指針指向 self,並在一小段時間(3秒)後,經過這個弱指針調用 -assertNotDealloc,而 -assertNotDealloc 主要做用是直接中斷言。

    - (BOOL)willDealloc {
       __weak id weakSelf = self;
       dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
           [weakSelf assertNotDealloc];
       });
       return YES;
    }
    - (void)assertNotDealloc {
       NSAssert(NO, @「」);
    }
    複製代碼

    這樣,當咱們認爲某個對象應該要被釋放了,在釋放前調用這個方法,若是3秒後它被釋放成功,weakSelf 就指向 nil,不會調用到 -assertNotDealloc 方法,也就不會中斷言,若是它沒被釋放(泄露了),-assertNotDealloc 就會被調用中斷言。這樣,當一個 UIViewController 被 pop 或 dismiss 時(咱們認爲它應該要被釋放了),咱們遍歷該 UIViewController 上的全部 view,依次調 -willDealloc,若3秒後沒被釋放,就會進入斷言。

參考文檔

www.jianshu.com/p/c448a4b4c…

www.jianshu.com/p/c3344193c…

www.jianshu.com/p/40ec2cae2…

相關文章
相關標籤/搜索