“理解”iOS記憶體管理

lyxia_iOS發表於2019-02-22

引言:
我們都知道Objective-C通過“引用計數”來管理物件釋放。基本原理就是管理物件的持有者個數(引用計數),引用計數為0時釋放物件。現在有ARC(自動引用計數),則無需我們自己顯式持有(retain)和釋放(release)物件,ARC通過對對像加上所有權修飾符(__strong等),編譯器通過物件的所有權修飾符將會自動鍵入引用計數管理(根據所有權修飾符自動鍵入retain、release、autorelease)

本文主要敘述引用計數的實現原理,ARC和MRC在使用上的區別,以及編譯器在ARC中為我們做了什麼。

Objective-C物件的MRC

  • retain
  • release
  • autorelease

autorelease的實現

使用棧(後進先出)來管理NSAutoreleasePool物件,因此可以隨時拿到最近(hotPage)的NSAutoreleasePool,呼叫物件的autorelease方法,會在hotPage中的內部陣列將物件加入進去。當NSAutoreleasePool出棧時,呼叫內部陣列中元素的release方法即可。

retain/release的實現

引用計數表:使用雜湊表實現引用計數表,key為物件的地址的雜湊值,value為引用計數和記憶體塊地址。
retain:通過物件的地址在引用計數表中找到引用計數,如果retainCount超過最大值,則拋異常,否則retainCount加1。
release:通過物件的地址在引用計數表中找到引用計數,如果retainCount值為0,則拋異常。否則retainCount減1,如果retainCount減1後為0,則從引用計數表衝移除,並呼叫物件的dealloc方法。

Objective-C物件的ARC

Objective-C物件的ARC是通過所有權修飾符來管理物件的持有和釋放。所有權修飾符一共有4種:

  • __strong 修飾符,預設的修飾符
  • __weak 修飾符
  • __unsafe_unretained 修飾符
  • __autoreleasing 修飾符

__strong修飾符的實現

取得自己生成並且持有物件:
使用ARC:

{
id obj1 = [NSObject new];
//相當於
//id __strong obj1 = [NSObject new];
}複製程式碼

不使用ARC:

{
id obj1 = [NSObject new];
//在變數作用域結束時插入release
[obj1 release];
}複製程式碼

取得非自己生成並持有物件:
使用ARC:

- (void)create {
    test = [self object];
    NSLog(@"ARC------------------ARC");
    NSLog(@"after create count = %ld", _objc_rootRetainCount(test));
}

- (id)object {
    id obj = [[MyObject alloc] init];
    return obj;
}

- (void)printRetainCount {
    NSLog(@"ARC------------------ARC");
    NSLog(@"retain count = %ld", _objc_rootRetainCount(test));
}複製程式碼

不使用ARC:

- (void)create {
    if (test) {
        NSLog(@"MRC------------------MRC");
        NSLog(@"release");
        [test release];
    }
    test = [self object];
    [test retain];
    NSLog(@"MRC------------------MRC");
    NSLog(@"after create count = %ld", [test retainCount]);
}

- (id)object {
    id obj = [[MyObject alloc] init];
    [obj autorelease];
    return obj;
}

- (void)printRetainCount {
    NSLog(@"MRC------------------MRC");
    NSLog(@"retain count = %ld", [test retainCount]);
}複製程式碼

列印結果:

**2016-12-13 15:35:47.545 ARCLearn[53676:16388070] ARC------------------ARC**
**2016-12-13 15:35:47.545 ARCLearn[53676:16388070] after create count = 1**
**2016-12-13 15:35:47.546 ARCLearn[53676:16388070] MRC------------------MRC**
**2016-12-13 15:35:47.546 ARCLearn[53676:16388070] after create count = 2**
**2016-12-13 15:35:49.602 ARCLearn[53676:16388070] ARC------------------ARC**
**2016-12-13 15:35:49.602 ARCLearn[53676:16388070] retain count = 1**
**2016-12-13 15:35:49.603 ARCLearn[53676:16388070] MRC------------------MRC**
**2016-12-13 15:35:49.603 ARCLearn[53676:16388070] retain count = 1**
**2016-12-13 15:35:51.212 ARCLearn[53676:16388070] myobject dealloc**
**2016-12-13 15:35:51.213 ARCLearn[53676:16388070] ARC------------------ARC**
**2016-12-13 15:35:51.214 ARCLearn[53676:16388070] after create count = 1**
**2016-12-13 15:35:51.214 ARCLearn[53676:16388070] MRC------------------MRC**
**2016-12-13 15:35:51.214 ARCLearn[53676:16388070] release**
**2016-12-13 15:35:51.215 ARCLearn[53676:16388070] myobject dealloc**
**2016-12-13 15:35:51.215 ARCLearn[53676:16388070] MRC------------------MRC**
**2016-12-13 15:35:51.215 ARCLearn[53676:16388070] after create count = 2**
**2016-12-13 15:36:03.540 ARCLearn[53676:16388070] ARC------------------ARC**
**2016-12-13 15:36:03.542 ARCLearn[53676:16388070] retain count = 1**
**2016-12-13 15:36:03.542 ARCLearn[53676:16388070] MRC------------------MRC**
**2016-12-13 15:36:03.543 ARCLearn[53676:16388070] retain count = 1**複製程式碼

可以看到使用ARC的物件持有,在返回物件時,並沒有把物件註冊到autoreleasepool中,下面為ARC的autoreleasepool列印:

**objc[54013]: ##############**
**objc[54013]: AUTORELEASE POOLS for thread 0x10c6ba3c0**
**objc[54013]: 13 releases pending.**
**objc[54013]: [0x7fa3c9811000]  ................  PAGE  (hot) (cold)**
**objc[54013]: [0x7fa3c9811038]  ################  POOL 0x7fa3c9811038**
**objc[54013]: [0x7fa3c9811040]    0x60800002e3c0  __NSCFString**
**objc[54013]: [0x7fa3c9811048]  ################  POOL 0x7fa3c9811048**
**objc[54013]: [0x7fa3c9811050]    0x7fa3ca800850  UIScreen**
**objc[54013]: [0x7fa3c9811058]    0x7fa3ca800850  UIScreen**
**objc[54013]: [0x7fa3c9811060]    0x6000002712c0  __NSCFDictionary**
**objc[54013]: [0x7fa3c9811068]    0x7fa3c8600bf0  UIWindow**
**objc[54013]: [0x7fa3c9811070]    0x60000008e6f0  __NSMallocBlock__**
**objc[54013]: [0x7fa3c9811078]    0x6000000567d0  __NSSetM**
**objc[54013]: [0x7fa3c9811080]    0x600000052de0  __NSSetM**
**objc[54013]: [0x7fa3c9811088]    0x600000053b30  __NSSetM**
**objc[54013]: [0x7fa3c9811090]    0x600000271640  __NSCFString**
**objc[54013]: [0x7fa3c9811098]    0x600000271640  __NSCFString**
**objc[54013]: ##############**複製程式碼

下面為MRC的autoreleasepool列印:

**objc[54013]: ##############**
**objc[54013]: AUTORELEASE POOLS for thread 0x10c6ba3c0**
**objc[54013]: 14 releases pending.**
**objc[54013]: [0x7fa3c9811000]  ................  PAGE  (hot) (cold)**
**objc[54013]: [0x7fa3c9811038]  ################  POOL 0x7fa3c9811038**
**objc[54013]: [0x7fa3c9811040]    0x60800002e3c0  __NSCFString**
**objc[54013]: [0x7fa3c9811048]  ################  POOL 0x7fa3c9811048**
**objc[54013]: [0x7fa3c9811050]    0x7fa3ca800850  UIScreen**
**objc[54013]: [0x7fa3c9811058]    0x7fa3ca800850  UIScreen**
**objc[54013]: [0x7fa3c9811060]    0x6000002712c0  __NSCFDictionary**
**objc[54013]: [0x7fa3c9811068]    0x7fa3c8600bf0  UIWindow**
**objc[54013]: [0x7fa3c9811070]    0x60000008e6f0  __NSMallocBlock__**
**objc[54013]: [0x7fa3c9811078]    0x6000000567d0  __NSSetM**
**objc[54013]: [0x7fa3c9811080]    0x600000052de0  __NSSetM**
**objc[54013]: [0x7fa3c9811088]    0x600000053b30  __NSSetM**
**objc[54013]: [0x7fa3c9811090]    0x600000271640  __NSCFString**
**objc[54013]: [0x7fa3c9811098]    0x600000271640  __NSCFString**
**objc[54013]: [0x7fa3c98110a0]    0x608000017200  MyObject**
**objc[54013]: ##############**複製程式碼

在autoreleasepool裡有MyObject物件。
為什麼ARC沒有把物件放到autoreleasepool裡了?它是怎樣持有非自己生成的物件的?
程式碼:

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

轉換為編譯器模擬程式碼:

{
    id obj = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);
}複製程式碼

objc_retainAutoreleasedReturnValue函式,顧名思義:讓obj持有(retain)池中(autoreleased)返回的值(retuenValue);
程式碼:

+ (id)array {
    return [[NSMutableArray alloc] init];
}複製程式碼

轉換為編譯器模擬程式碼:

+ (id)array {
    id obj = objc_msgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}複製程式碼

objc_autoreleaseReturnValue函式,顧名思義:把obj註冊在池中(呼叫obj的autorelease方法),並返回。
因此按照上面的理解objc_autoreleaseReturnValue將返回物件註冊到池子中,objc_retainAutoreleasedReturnValue`持有池中的物件。

但是事實不是那麼簡單:
但是objc_autoreleaseReturnValue遠遠不是autorelease那麼簡單。objc_autoreleaseReturnValue函式會檢查使用該函式的方法或函式呼叫方的執行命令列表,如果方法或函式的呼叫方在呼叫了方法或函式後緊接著呼叫了objc_retainAutoreleasedReturnValue,那就不會將返回的物件註冊到autoreleasepool中,而是直接傳遞到方法或函式的呼叫方。
因此objc_retainAutoreleasedReturnValue也不是retain那麼簡單,即使放回物件不註冊到autoreleasepool中,也能正確的獲取物件。
通過objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue協作,可以不將物件註冊到autoreleasepool中而直接傳遞,這一過程達到了最優化。

__weak修飾符的實現

現在我們新增一個weakTest變數,使用weak所有權修飾符
在ARC中實現:

@interface ARC() {
    id test;
    id __weak weakTest;
}

@end

@implementation ARC

- (void)create {
    test = [self object];
    weakTest = test;
    _objc_autoreleasePoolPrint();
    NSLog(@"ARC------------------ARC");
    NSLog(@"after create count = %ld", _objc_rootRetainCount(test));
}

- (id)object {
    id obj = [[MyObject alloc] init];
    return obj;
}

- (void)printRetainCount {
    NSLog(@"ARC------------------ARC");
    NSLog(@"retain count = %ld", _objc_rootRetainCount(test));
}

@end複製程式碼

列印結果:

**2016-12-13 16:24:30.934 ARCLearn[55038:16489628] ARC------------------ARC**
**2016-12-13 16:24:30.935 ARCLearn[55038:16489628] after create count = 1**複製程式碼

由此可以看出__weak並沒有增加引用個數。
新增empty函式,使test為nil:

- (void)empty {
    if (weakTest) {
        NSLog(@"weak is %p", weakTest);
    }
    test = nil;
    if (!weakTest) {
        NSLog(@"weak change to nil");
    }
}複製程式碼

列印結果:

**2016-12-13 16:33:06.301 ARCLearn[55349:16509066] weak is 0x608000011530**
**2016-12-13 16:33:06.301 ARCLearn[55349:16509066] myobject dealloc**
**2016-12-13 16:33:06.302 ARCLearn[55349:16509066] weak change to nil**複製程式碼

由此可以看出若附有__weak修飾符的變數所引用的物件被廢棄,則將nil賦值給該變數
是如何實現將nil值賦值給引用為0的__weak物件?

{
    id __weak obj1 = obj;
}複製程式碼

轉換為編譯器模擬程式碼:

{
    id obj1;
    objc_initWeak(&obj1, obj);
    objc_destroyWeak(obj1);
}複製程式碼

其中objc_initWeak(&obj1,obj)的實現:

obj1 = 0;
objc_storyWeak(&obj1, obj);複製程式碼

objc_destroyWeak的實現:

objc_storyWeak(&obj1, 0);複製程式碼

所以合在一起是:

{
    id obj1;
    obj1 = 0;
    objc_storyWeak(&obj1, obj);
    objc_storyWeak(&obj1, 0);
}複製程式碼

其中objc_storyWeak就是針對weak表(雜湊表)操作:

  • 註冊到表中:objc_storeWeak函式把第二個引數的地址(雜湊值)作為鍵,將第一個引數的地址加入連結串列中(鍵值為一個連結串列,由於一個物件可同時賦值給多個附有__weak修飾符的變數),完成weak表的註冊。
  • 從表中移除:如果第二個引數為0,則把第二個引數的地址的鍵從weak表中刪除,並且將鍵值置為nil(將連結串列中的值都置為nil)。
  • 從表中查詢:使用weak表,將廢棄物件的地址作為鍵進行檢索,就能告訴的獲取對應的附有__weak修飾符的變數的地址。

廢棄引用為0的物件的流程:

  • objc_release
  • 因為引用計數為0所以執行dealloc
  • 從weak表中獲取廢棄物件的地址為鍵的記錄
  • 將包含在記錄中所有附有__weak修飾符變數的地址,賦值為nil
  • 從weak表中刪除該記錄
  • 從引用計數表中刪除廢棄物件的地址為鍵的記錄

__autoreleasing修飾符的實現

將物件賦值給附有__autoreleasing修飾符的變數等同於ARC無效時呼叫物件的autorelease方法。
ARC有效:

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];

    id __strong obj1 = [[NSObject alloc] init];
    id __autoreleasing obj2 = obj1;

    id __strong obj3 = [NSMutableArray array];
    id __autoreleasing obj4 = obj3;
}複製程式碼

ARC無效:

@autoreleasepool {
    id obj = [[NSObject alloc] init];
    [obj autorelease];

    id obj1 = [[NSObject alloc] init];
    id obj2 = obj1;
    [obj2 retain];
    [obj2 autorelease];

    id obj3 = [NSMutableArray array];
    [obj3 retain];
    id obj4 = obj3;
    [obj4 retain];
    [obj4 autorelease];
    [obj3 release];
}複製程式碼

注意:顯式的附加autoreleasing修飾符同顯式地新增strong修飾符一樣罕見。我們經常非顯式的使用__autoreleasing修飾符。

隱式使用__autoreleasing:

  • 編譯器檢查方法名是否以alloc/new/copy/mutableCopy開始,如果不是則自動將返回值的物件註冊到autoreleasepool(objc_retainAutoreleasedReturnValue和objc_autoreleaseReturnValue成對出現時就不會註冊到autoreleasepool)。
  • id的指標或物件的指標在沒有顯式指定時會被附加上__autoreleasing修飾符。

如:

NSError *error = nil;
BOOL result = [obj performOperationWithError: &error];複製程式碼

performOperationWithError宣告為:

- (BOOL)performOperationWithError:(NSError **)error;複製程式碼

自動新增__autoreleasing的程式碼:

- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error {
    /* 錯誤發生 */
    *error = [[NSError alloc] initwithDomain:MyAppDomain code:errorCode userInfo:nil];
    return NO;
}複製程式碼

非ARC的實現:

- (BOOL)performOperationWithError:(NSError **)error {
    /* 錯誤發生 */
    *error = [[NSError alloc] initwithDomain:MyAppDomain code:errorCode userInfo:nil];
    [*error autorelease];
    return NO;
}複製程式碼

因為宣告為NSError __autoreleasing 型別的error作為*error被賦值,所以能夠返回註冊到autoreleasepool中的物件。

注意:賦值給物件指標時,所有權修飾符必須一致。
編譯器會自動加上所有權修飾符:

NSError * __strong error = nil;
NSError * __autoreleasing *pError = &error;複製程式碼

所有權修飾符不一致會產生編譯錯誤:
Initializing `NSError *__autoreleasing *` with an expression of type `NSError *__strong *` changes retain/release properties of pointer

__unsafe_unretained 修飾符

unsafe_unretained修飾符正如其名unsafe所示,是不安全的所有權修飾符。儘管ARC式的記憶體管理是編譯器的工作,但附有unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理物件。
ARC實現:

id obj = [[NSObject alloc] init];
id __unsafe_unretained obj1 = obj;複製程式碼

非ARC實現:

id obj = [[NSObject alloc] init];
id obj1 = obj;複製程式碼

所以就是…編譯器啥都不做。這樣當物件的引用計數為0,被廢棄後,obj1就是不安全的了(懸垂指標),因為不會被置為nil。

注意:使用時確保物件確實存在。

什麼時候使用__unsafe_unretained?

  • 在iOS4的應用程式中,必須使用unsafe_unretained修飾的變數,來替代weak修飾符。
  • 不支援__weak修飾符的類,例如NSMachPork類,因為這些類重寫了retain/release並實現該類獨自的引用計數機制。
  • allowsWeakReference/retainWeakReference例項方法(沒有寫入NSObject介面說明文件中)返回NO的情況。

Core Foundation物件的ARC

Core Foundation物件主要使用在用C語言編寫的Core Foundation框架中,並使用引用計數的物件。

  • 在ARC無效時,Core Foundation框架中的retain/release分別是CFRetain/CFRelease。
  • Foundation框架的API生成並持有的物件可以用Core Foundation框架的API釋放。當然,反過來也是可以的。
  • 因為Core Foundation物件與Objective-C物件沒有區別,所以ARC無效時,只用簡單的C語言的轉換也能實現互換。因為這種轉換不需要使用額外的CPU資源,因此也被稱為“免費橋”(Toll-free Bridge)。

ARC無效時:

id obj = [[NSObject alloc] init];
void *p = obj; //void *相當與oc的id

id o = p;
[o release];複製程式碼

ARC有效時,會引起編譯錯誤,要使用“__bridge”轉換。
ARC有效時的程式碼:

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

id o = (__bridge id)p;
[o release];複製程式碼

注意:bridge的安全性與賦值給unsafe_unretained修飾符相近,甚至會更低,如果管理時不注意賦值物件的所有者,就會因懸垂指標而導致程式崩潰。

__bridge轉換中還有另兩中轉換,分別為:

  • bridge_retained轉換,相當於賦值給strong變數。
    ARC有效:
    id obj = [[NSObject alloc] init];
    void *p = (__bridge_retained void*)obj;複製程式碼

    ARC無效:

    id obj = [[NSObject alloc] init];
    void *p = obj;
    [(id)p retain];複製程式碼
  • bridge_transfer轉換,相當於賦值給strong變數後,該變數隨之釋放。
    ARC有效
    void *p = 0;
    id obj = (__bridge_transfer id)p;複製程式碼

    ARC無效

    void *p = 0;
    id obj = (id)p;
    [obj retain];
    [(id)p release];複製程式碼

簡書個人主頁:www.jianshu.com/users/b92ab…

相關文章