你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 深入理解Objective C的ARC機制

深入理解Objective C的ARC機制

編輯:IOS開發基礎

timg (3).jpg

前言

本文的ARC特指Objective C的ARC,並不會講解其他語言。另外,本文涉及到的原理部分較多,適合有一定經驗的開發者。

什麼是ARC?

ARC的全稱Auto Reference Counting. 也就是自動引用計數。那麼,為什麼要有ARC呢?

我們從C語言開始。使用C語言編程的時候,如果要在堆上分配一塊內存,代碼如下

//分配內存(malloc/calloc均可)
int * array = calloc(10, sizeof (int));
//釋放內存
free(array);1234512345


C是面向過程的語言(Procedural programming),這種內存的管理方式簡單直接。但是,對於面向對象編程,這種手動的分配釋放毫無疑問會大大的增加代碼的復雜度。

於是,OOP的語言引入了各種各樣的內存管理方法,比如Java的垃圾回收和Objective C的引用計數。關於垃圾回收和飲用計數的對比,可以參見Brad Larson的這個SO回答。

Objective C的引用計數理解起來很容易,當一個對象被持有的時候計數加一,不再被持有的時候引用計數減一,當引用計數為零的時候,說明這個對象已經無用了,則將其釋放。

引用計數分為兩種:

  • 手動引用計數(MRC)

  • 自動引用計數(ARC)

iOS開發早期,編寫代碼是采用MRC的

// MRC代碼
NSObject * obj = [[NSObject alloc] init]; //引用計數為1
//不需要的時候
[obj release] //引用計數減1
//持有這個對象
[obj retain] //引用計數加1
//放到AutoReleasePool
[obj autorelease]//在auto release pool釋放的時候,引用計數減1

雖說這種方式提供了面向對象的內存管理接口,但是開發者不得不花大量的時間在內存管理上,並且容易出現內存洩漏或者release一個已被釋放的對象,導致crash。

再後來,Apple對iOS/Mac OS開發引入了ARC。使用ARC,開發者不再需要手動的retain/release/autorelease. 編譯器會自動插入對應的代碼,再結合Objective C的runtime,實現自動引用計數。

比如如下ARC代碼:

NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用計數為1
}
NSLog(@"%@",obj);

等同於如下MRC代碼

NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用計數為1
    [obj relrease]
}
NSLog(@"%@",obj);

在Objective C中,有三種類型是ARC適用的:

  • block

  • objective 對象,id, Class, NSError*等

  • attribute((NSObject))標記的類型。

像double *,CFStringRef等不是ARC適用的,仍然需要手動管理內存。

Tips: 以CF開頭的(Core Foundation)的對象往往需要手動管理內存。

屬性所有權

最後,我們在看看ARC中常見的所有權關鍵字,

  • assign對應關鍵字__unsafe_unretained, 顧名思義,就是指向的對象被釋放的時候,仍然指向之前的地址,容易引起野指針。

  • copy對應關鍵字__strong,只不過在賦值的時候,調用copy方法。

  • retain對應__strong

  • strong對應__strong

  • unsafe_unretained對應__unsafe_unretained

  • weak對應__weak。

其中,__weak和__strong是本文要講解的核心內容。

ARC的內部實現

ARC背後的引用計數主要依賴於這三個方法:

  • retain 增加引用計數

  • release 降低引用計數,引用計數為0的時候,釋放對象。

  • autorelease 在當前的auto release pool結束後,降低引用計數。

在Cocoa Touch中,NSObject協議中定義了這三個方法,由於Cocoa Touch中,絕大部分類都繼承自NSObject(NSObject類本身實現了NSObject協議),所以可以“免費”獲得NSObject提供的運行時和ARC管理方法,這就是為什麼適用OC開發iOS的時候,你的類要繼承自NSObject。

既然ARC是引用計數,那麼對應一個對象,內存中必然會有一個地方來存儲這個對象的引用計數。iOS的Runtime是開源的,在這裡可以下載到全部的代碼,我們通過源代碼一探究竟。

我們從retain入手,

- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

所以說,本質上retain就是調用sidetable_retain,再看看sitetable_retain的實現:

id objc_object::sidetable_retain()
{
    //獲取table
    SideTable& table = SideTables()[this];
    //加鎖
    table.lock();
    //獲取引用計數
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
         //增加引用計數
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //解鎖
    table.unlock();
    return (id)this;
}

到這裡,retain如何實現就很清楚了,通過SideTable這個數據結構來存儲引用計數。我們看看這個數據結構的實現:

QQ截圖20170421165138.png

可以看到,這個數據結構就是存儲了一個自旋鎖,一個引用計數map。這個引用計數的map以對象的地址作為key,引用計數作為value。到這裡,引用計數的底層實現我們就很清楚了。

存在全局的map,這個map以地址作為key,引用計數的值作為value。

再來看看release的實現:

    SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    table.lock();
    //找到對應地址的
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) { //找不到的話,執行dellloc
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用計數小於阈值,dealloc
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
    //引用計數減去1
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        //執行dealloc
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;

release的到這裡也比較清楚了:查找map,對引用計數減1,如果引用計數小於阈值,則調用SEL_dealloc

Autorelease pool

上文提到了,autorelease方法的作用是把對象放到autorelease pool中,到pool drain的時候,會釋放池中的對象。舉個例子

    __weak NSObject * obj;
    NSObject * temp = [[NSObject alloc] init];
    obj = temp;
    NSLog(@"%@",obj); //非空

放到auto release pool中,

    __weak NSObject * obj;
    @autoreleasepool {
        NSObject * temp = [[NSObject alloc] init];
        obj = temp;
    }
    NSLog(@"%@",obj); //null

可以看到,放到自動釋放池的對象是在超出自動釋放池作用域後立即釋放的。事實上在iOS 程序啟動之後,主線程會啟動一個Runloop,這個Runloop在每一次循環是被自動釋放池包裹的,在合適的時候對池子進行清空。

對於Cocoa框架來說,提供了兩種方式來把對象顯式的放入AutoReleasePool.

  • NSAutoreleasePool(只能在MRC下使用)

  • @autoreleasepool {}代碼塊(ARC和MRC下均可以使用)

那麼AutoRelease pool又是如何實現的呢?

我們先從autorelease方法源碼入手

//autorelease方法
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}
//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    //檢查是否可以優化
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    //放到auto release pool中。
    return rootAutorelease2();
}
// rootAutorelease2
id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

可以看到,把一個對象放到auto release pool中,是調用了AutoreleasePoolPage::autorelease這個方法。

我們繼續查看對應的實現:

public: static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }
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 {
            return autoreleaseNoPage(obj);
        }
    }
id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

到這裡,autorelease方法的實現就比較清楚了,

autorelease方法會把對象存儲到AutoreleasePoolPage的鏈表裡。等到auto release pool被釋放的時候,把鏈表內存儲的對象刪除。所以,AutoreleasePoolPage就是自動釋放池的內部實現。

__weak與__strong

用過block的同學一定寫過類似的代碼:

__weak typeSelf(self) weakSelf = self;
[object fetchSomeFromRemote:^{
    __strong typeSelf(weakSelf) strongSelf = weakSelf;
    //從這裡開始用strongSelf
}];

那麼,為什麼要這麼用呢?原因是:

block會捕獲外部變量,用weakSelf保證self不會被block被捕獲,防止引起循環引用或者不必要的額外生命周期。

用strongSelf則保證在block的執行過程中,對象不會被釋放掉。

首先__strong和__weak都是關鍵字,是給編譯器理解的。為了理解其原理,我們需要查看它們編譯後的代碼,使用XCode,我們可以容易的獲得一個文件的匯編代碼。

比如,對於Test.m文件,當源代碼如下時:

 #import "Test.h"
 @implementation Test
- (void)testFunction{
    {
        __strong NSObject * temp = [[NSObject alloc] init];
    }
}
@end

轉換後的匯編代碼如下:

Ltmp3:
    .loc    2 15 37 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:37
    ldr     x9, [x9]
    ldr     x1, [x8]
    mov  x0, x9
    bl  _objc_msgSend
    adrp    x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE
    add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
    .loc    2 15 36 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36
    ldr     x1, [x8]
    .loc    2 15 36 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36
    bl  _objc_msgSend
    mov x8, #0
    add x9, sp, #8              ; =8
    .loc    2 15 29                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:29
    str x0, [sp, #8]
Ltmp4:
    .loc    2 16 5 is_stmt 1        ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5
    mov  x0, x9
    mov  x1, x8
    bl  _objc_storeStrong
    .loc    2 17 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1
    ldp x29, x30, [sp, #32]     ; 8-byte Folded Reload
    add sp, sp, #48             ; =48
    ret
Ltmp5:

即使你不懂匯編,也能很輕易的獲取到調用順序如下

_objc_msgSend // alloc
_objc_msgSend // init
_objc_storeStrong // 強引用

在結合Runtime的源碼,我們看看最關鍵的objc_storeStrong的實現

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}
id objc_retain(id obj) { return [obj retain]; }
void objc_release(id obj) { [obj release]; }

我們再來看看__weak. 將Test.m修改成為如下代碼,同樣我們分析其匯編實現

    .loc    2 15 35 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:35
    ldr     x9, [x9]
    ldr     x1, [x8]
    mov  x0, x9
    bl  _objc_msgSend
    adrp    x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE
    add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
    .loc    2 15 34 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34
    ldr     x1, [x8]
    .loc    2 15 34 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34
    bl  _objc_msgSend
    add x8, sp, #24             ; =24
    .loc    2 15 27                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    mov  x1, x0
    .loc    2 15 27 discriminator 2 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    str x0, [sp, #16]           ; 8-byte Folded Spill
    mov  x0, x8
    bl  _objc_initWeak
    .loc    2 15 27                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    ldr x1, [sp, #16]           ; 8-byte Folded Reload
    .loc    2 15 27 discriminator 3 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    str x0, [sp, #8]            ; 8-byte Folded Spill
    mov  x0, x1
    bl  _objc_release
    add x8, sp, #24  
    Ltmp4:
    .loc    2 16 5 is_stmt 1        ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5
    mov  x0, x8
    bl  _objc_destroyWeak
    .loc    2 17 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1
    ldp x29, x30, [sp, #48]     ; 8-byte Folded Reload
    add sp, sp, #64             ; =64
    ret

可以看到,__weak本身實現的核心就是以下兩個方法

  • _objc_initWeak

  • _objc_destroyWeak

我們通過Runtime的源碼分析這兩個方法的實現:

id objc_initWeak(id *location, id newObj)
{
    //省略....
    return storeWeak        (location, (objc_object*)newObj);
}
void objc_destroyWeak(id *location)
{
    (void)storeWeak        (location, nil);
}

所以,本質上都是調用了storeWeak函數,這個函數內容較多,主要做了以下事情

  • 獲取存儲weak對象的map,這個map的key是對象的地址,value是weak引用的地址。

  • 當對象被釋放的時候,根據對象的地址可以找到對應的weak引用的地址,將其置為nil即可。

這就是在weak背後的黑魔法。

總結

這篇文章屬於想到哪裡寫到哪裡的類型,後邊有時間了在繼續總結ARC的東西吧。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved