淺析OC內存管理

前言:本篇內容假設您已經對內存管理有了基礎的理解。如retain、release、autorelease、autoreleasepool的釋放及使用、引用計數式內存管理概念等。本篇內容將圍內存管理進行一些深入的探究。

一、內存管理的思考方式
1.1、內存管理的黃金法則

  •       自己生成的對象自己持有。
    
  •        非自己生成的對象,自己也能持有。
    
  •       不再需要自己持有的對象時釋放。
    
  •       非自己持有的對象無法釋放。
        以上出現了“生成”、“持有”、“釋放”三個詞。而在OC內存管理中還要加上“廢棄”一詞。
    

對象操作與OC方法的對應


對象操作與oc方法的對應.jpeg

1.2、命名規則
生成并持有對象的方法名命名規則是遵循駝峰設計法。下列名稱意味著自己生成并持有對象。

  • allocMyObject
  • newThatObject
  • copyThis
  • mutableCopyYourObject

但是對于以下名稱,并不在該規則范圍內。

  • allocate

  • newer

  • copyingThis
    開發過程中必須嚴格遵守內存管理命名規則,編譯器會根據方法名做出相應的處理。

    1.3、關于生成并持有對象與生成不持有對象的實現
    那么,如果要用某個方法生成對象,并將其返還給該方法的調用方,它的源代碼又是怎樣的呢?
    -(id)allocObject{
    //自己成并持有對象
    id obj = [[NSObject alloc]init];
    //自己持有對象
    return obj;
    }
    如上例所示,原封不動的返回用alloc方法生成的對象,就能讓調用方也持有該對象。

那么,如果類似調用【NSMutableArray array】方法使取得的對象存在,但自己不持有對象。又是如何實現的呢?
-(id)object{
//自己持有對象
id obj = [[NSObject alloc]init];
//將對象放入自動釋放池
[obj autorelease];
//取得對象存在,但自己不持有對象
return obj;
}
上例中(注意,方法名不能以alloc/new/copy/mutableCopy等名稱開頭),使用了autorelease方法。用該方法,可以使取得的對象存在,但自己不持有對象。autorelease提供這樣的功能,使對象在超出指定生存范圍時能夠自動并正確的釋放。當然,也能夠通過retain方法將調用autorelease方法取得的對象變為自己持有。

二、ARC規則
2.1、所有權修飾符
OC中為了處理對象,可將變量類型定義為id類型或各種對象類型。
所謂對象類型就是指向NSObject這樣的OC類的指針,例如“NSObject *”。id類型用于隱藏對象類型的類名部分,相當于C語言中的“void *
ARC有效時,id類型和對象類型同C語言的其他類型不同,其類型上必須附件所有權修飾符。

  • __strong 修飾符
    
  • __weak 修飾符
    
  • __unsafe_unretained 修飾符
    
  • __autoreleasing 修飾符
    

    __strong修飾符
    __srong修飾符是id類型和對象類型默認的所有權修飾符。在ARC中,附有__strong修飾符的變量obj在超出其變量作用域時,即在該變量被廢棄時,會釋放其被賦予的對象。通過__strong修飾符,不必再次鍵入retain或者release即可滿足完美地滿足“引用計數式內存管理的思考方式”。另外,__strong修飾符同后面的__weak修飾符和__autoreleasing修飾符都可以保證將附有這些修飾符的自動變量初始化為nil。

    __weak修飾符
    眾所周知,當兩個OC對象互相強引用時會產生循環引用,而打破循環引用的利器就是__weak修飾符(弱引用)。__weak修飾符還有另一個有點。在持有某對象的弱引用時,若該對象被廢棄,則此弱引用將自動失效且處于nil被賦值的狀態,從而有效的避免野指針(指向已被釋放內存的指針)的發生。
    在底層實現上,編譯器會維護一個weak表來實現,對象廢棄將弱引用指針賦值為nil的操作。
    weak表是以散列表的方式來實現的。其key為賦值對象的地址,value為被__weak修飾符修飾的變量。在賦值對象被廢棄時最后會執行以下操作以確保__weak修飾符修飾的變量被置為nil。

  • 從weak表中獲取廢棄對象的地址作為key的記錄。

  • 將包含在記錄中的所有附有__weak修飾符變量的地址賦值為nil。

  • 從weak表中刪除該記錄。

  • 從引用計數表中刪除廢棄對象的地址為key的記錄。

  • 廢棄對象
    由此可知,如果大量使用附有__weak修飾符的變量,則會消耗相應的CPU資源。良策是只在需要避免循環引用時使用__weak修飾符。

    __unsafe_unretained 修飾符
    __unsafe_unretained修飾符正如其名,是不安全的所有權修飾符。附有__unsafe_unretained修飾符的變量不屬于編譯器的內存管理對象。__unsafe_unretained與__weak修飾符一樣不會持有對象的強引用。其不同之處在于,若該對象被廢棄,編譯器不會對被__unsafe_unretained修飾的變量做任何處理從而產生野指針。

    __autoreleasing 修飾符
    關于__autoreleasing修飾符可以理解為,在ARC有效時,用@autoreleasepool塊替代NSAutoreleasePool 類,用附有__autoreleasing修飾符的變量替代autorelease方法。日常開發中我們雖然很少顯式使用__autoreleasing修飾符,但是編譯器的確通過隱式的使用__autoreleasing修飾符完成內存管理的工作。例如:

  • 如之前提到的命名規則,編譯器會檢查方法名是否以alloc/new/copy/mutableCopy等名稱開頭,如果不是則自動將返回的對象以__autoreleasing修飾符修飾(注冊到autoreleasepool)。

  • 雖然__weak修飾符是為了避免循環引用而使用的,但在訪問附有__weak修飾符的變量時,即是使用注冊到autoreleasepool中的對象。這是因為__weak修飾符只持有對象的弱引用,而在訪問對象的弱引用的過程中,該對象有可能會被廢棄。如果把要訪問的對象注冊到autoreleasepool中,那么在@autoreleasepool塊結束之前都能確保該對象的存在。因此,在使用附有__weak修飾符的變量時,即是使用注冊到autoreleasepool中的對象。但是,如果大量地使用附有__weak 修飾符的變量,注冊到autoreleasepool的對象也會大量的增加,因此在使用附有__weak修飾符的變量時,最好先暫時的賦值給附有__strong修飾符的變量后使用。

  •   id的指針(id *)或對象的指針(NSObject * *)在沒有顯示指定時會被附加上__autoreleasing修飾符。比如,為了得到錯誤的詳細信息,經常會在方法的參數中傳遞NSError對象的指針,而不是函數的返回值。Cocoa的框架中大多數方法也使用這種方式。                                                                             
    

    ```
    NSError * error = nil;
    BOOL result = [obj performOperationWithError:&error];
    ```
    該方法的聲明為:

    -(BOOL)performOperationWithError:(NSError **)error;

      如同上面的描述一樣,id的指針和對象的指針會默認附加上__autoreleasing修飾符,所以等同于一下源代碼:
    

    -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

      參數中持有NSError對象指針的方法,雖然為響應其執行結果,需要生成NSError類對象,但也必須符合內存管理的思考方式。以alloc/new/copy/mutableCopy等名稱開頭的方法返回值取得的對象是自己生成并持有的,其他情況下便是取得非自己生產并持有的對象。使用__autoreleasing修飾符的變量作為對象取得參數,與除alloc/new/copy/mutableCopy外其他方法返回的對象一樣都會注冊到autoreleasepool,并取得非自己生產并持有的對象。
    
      比如performOperationWithError 的實現應該是這樣:
    

-(BOOL)performOperationWithError:(NSError * __autoreleasing )error{
//生成的
error 將被注冊到autoreleasepool
*error = [[NSError alloc]initWithDomain:(ErrorDomain) code:code userInfo:nil];
return YES;
}

在使用參數取得對象時,貫徹內存管理的思考方式,我們需要將參數聲明為附有__autoreleasing修飾符的對象指針類型。

2.2、顯式轉換id 和 void *
在ARC無效時id 型變量和 void * 變量互相賦值(強制轉換)是沒有任何問題的。但是在ARC有效時,id 型變量和 void * 變量互相賦值時需要進行特定的轉換。轉換的方式我們稱之為“橋接”,橋接的方式有三種:

  • __bridge 轉換,如果只是單純的賦值操作,可以使用“__bridge轉換”

    id obj = [[NSObject alloc]init];
    void * p = (__bridge void *)obj;
    id o = (__bridge id)p;

    但是,“__bridge 轉換”的安全性與賦值給__unsafe_unretained修飾符相近,甚至會更低。如果管理時不注意賦值對象的所有者,就會因為野指針而導致程序崩潰。

  • __bridge_retain 轉換,可以使要轉換賦值的變量也持有所賦值的對象。另外,__bridge_retain 只提供了將 id 類型變量轉換為 void * 變量的能力(即 oc -> c)。
    void * p = 0;
    {
    //obj 指向對象 引用計數 1
    id obj = [[NSObject alloc]init];
    //p 持有 obj 指向對象 引用計數二
    p = (__bridge_retained void *)obj;

    }
    //obj 釋放 引用計數減一
    //訪問 p 指向對象
    NSLog(@"class = %@",[(__bridge id)p class]);

變量作用域結束時,雖然obj失效,對象引用計數減一。但由于__bridge_retain 轉換使變量p仍然處于持有該對象的狀態,因此該對象不會被廢棄。

  • __bridge_transfer 轉換,提供了與__bridge_retain相反的動作,被轉換的變量所持有的對象在該變量被賦值給轉換目標變量后隨即釋放對對象的強引用。另外 __bridge_transfer 只提供了將 void * 變量轉換為 id 類型變量的能力
    // p 指向創建的對象 引用計數為1
    void * p = (__bridge_retained void *)[[NSObject alloc]init];
    // obj 指向對象的同時 p釋放對對象的強引用 引用計數依然為1
    id obj = (__bridge_transfer id)p;

同__bridge_retain轉換與retain類似,__bridge_transfer轉換與release類似。

2.3、Objective-C對象 與 Core Foundation對象

    在2.2 提到的“橋接轉換”多數使用在OC對象與Core Foundation對象之間的相互變換中。OC對象與Core Foundation對象的區別很小,不同之處只在于是由哪一個框架所生成的。無論是由哪一種框架所生成的對象,一旦生成之后,便能在不同的框架之中使用。Foundation框架的API生成的對象可以用Core Foundation框架的API釋放。當然反過來也是可以的。
    因為Core Foundation 和 OC 對象沒有區別,所以在ARC無效時,OC變量和Core Foundation變量之間可以相互賦值。而ARC是基于NSObject的 無法管理Core Foundation的內存,所以就需要一些特定的轉換。除了 “橋接轉換之外” Core Foundation 提供了兩個類似的函數進行OC 對象 與Core Foundation對象之間的轉換。
  • CFBridgingRetain(<id _Nullable X>) 其功能與 __bridge_retain 一致

    CFMutableArrayRef cfObj = NULL;
    {
    // 變量 obj 持有 對生成對象的強引用 引用計數1
    NSMutableArray * obj = [[NSMutableArray alloc]init];
    //通過CFBridgingRetain 將 對象CFRetain并賦值給變量cfObj 引用計數為2
    cfObj = (CFMutableArrayRef) CFBridgingRetain(obj);

    }
    //obj 超出作用域 其強引用失效 引用計數 1

    //將該對象CFRelease 引用計數0 釋放對象
    CFRelease(cfObj);

  • CFBridgingRelease(<CFTypeRef  _Nullable X>) 其功能與 __bridge_transfer 一致
    

{
//Core Foundation 框架生成對象 對象引用計數1
CFMutableArrayRef cfObj = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
//通過CFBridgingRelease賦值,變量obj持有對象的強引用的同時,cfObj 通過CFRelease釋放對象的強引用 對象引用計數1
NSMutableArray * obj = CFBridgingRelease(cfObj);

}
//obj 超出作用域 引用計數0 對象釋放

在OC對象與Core Foundation 對象 (id 對象 和 void * 對象)相互轉換時 必須恰當的使用CFBridgingRetain 和 CFBridgingRelease (__bridge_retain 和__bridge_transfer),否則會產生內存泄漏或者野指針。因此在實現代碼時一定要高度審視。

2.3最優化程序運行
如之前提到的命名規則,編譯器會檢查方法名是否以alloc/new/copy/mutableCopy等名稱開頭,如果不是則自動將返回的對象以__autoreleasing修飾符修飾。而在ARC中,編譯器通常會返回objc_autoreleaseReturnValue(obj)函數返回的的對象,而不是objc_autorelease(obj)。objc_autorelease(obj)的作用僅限于將對象注冊到autoreleasepool中。而objc_autoreleaseReturnValue(obj)的作用不僅限于注冊對象到autoreleasepool中。
objc_autoreleaseReturnValue函數會檢查使用該函數的方法或函數調用方的執行命令列表,如果方法或函數的調用在調用了方法或函數后緊接著調用了objc_retainAutoreleasedReturnValue函數,那么久不將返回的對象注冊到autoreleasepool中,而是直接傳遞到方法或函數的調用方。通過objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue函數的協作,可以不將對象注冊到autoreleasepool中而是直接傳遞,使這一過程達到最優。體現在代碼層類似這樣(以下為偽代碼):

+(id)array{
return [[NSMutableArray alloc]init];
}

以上代碼在ARC中會轉換為以下偽代碼:

+(id)array{
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_msgSend(obj,@selector(init));
return objc_autoreleaseReturnValue(obj);
}

而當[NSMutableArray array]方法的返回值賦給被__strong修飾符修飾的變量時,編譯器會調用objc_retainAutoreleasedReturnValue函數以避免將對象注冊到autoreleasepool中
{
id __strong obj = [NSMutableArray array];
}

以上代碼在ARC中會轉換為以下偽代碼:

id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);

//超出obj 作用域 釋放對象
objc_release(obj);

2.4屬性修飾符與所有權修飾符

  賦值給屬性修飾符修飾的屬性就相當于賦值給各屬性修飾符對應的所有權修飾符修飾的變量中。只有copy屬性不是簡單的賦值,它賦值的是通過NSCopying接口的copyWithZone:方法復制賦值源所生成的對象。以下為屬性修飾符與所有權修飾符的對應關系

屬性修飾符
所有權修飾符
assign
__unsafe_unretained修飾符
copy
__strong修飾符(但是賦值的是被復制的對象)
retain
__strong修飾符
strong
__strong修飾符
unsafe_unretained
__unsafe_unretained修飾符
weak
__weak修飾符

本篇對于OC內存管理進行了簡要的剖析,如有什么疑問或不對的地方,歡迎評價指正,共同討論共同進步。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容