Objective-C 引用計數原理

引用計數如何存儲

有些對象若是支持使用 TaggedPointer,蘋果會直接將其指針值做爲引用計數返回;若是當前設備是 64 位環境而且使用 Objective-C 2.0,那麼「一些」對象會使用其 isa 指針的一部分空間來存儲它的引用計數;不然 Runtime 會使用一張散列表來管理引用計數。 其實還有一種狀況會改變引用計數的存儲策略,那就是是否使用垃圾回收(用UseGC屬性判斷),但這種早已棄用的東西就不要管了,並且初始化垃圾回收機制的 void gc_init(BOOL wantsGC) 方法一直被傳入 NO。html

TaggedPointer

判斷當前對象是否在使用 TaggedPointer 是看標誌位是否爲 1 :算法

#if SUPPORT_MSB_TAGGED_POINTERS
# define TAG_MASK (1ULL<<63)
#else
# define TAG_MASK 1

inline bool 
objc_object::isTaggedPointer() 
{
#if SUPPORT_TAGGED_POINTERS
    return ((uintptr_t)this & TAG_MASK);
#else
    return false;
#endif
}
複製代碼

id 其實就是 objc_object * 的簡寫(typedef struct objc_object *id;),它的 isTaggedPointer() 方法常常會在操做引用計數時用到,由於這決定了存儲引用計數的策略。數組

isa 指針(NONPOINTER_ISA)

用 64 bit 存儲一個內存地址顯然是種浪費,畢竟不多有那麼大內存的設備。因而能夠優化存儲方案,用一部分額外空間存儲其餘內容。isa 指針第一位爲 1 即表示使用優化的 isa 指針,這裏列出不一樣架構下的 64 位環境中 isa 指針結構:bash

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA
# if __arm64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
        uintptr_t magic             : 9;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };

# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 14;
# define RC_ONE (1ULL<<50)
# define RC_HALF (1ULL<<13)
    };

# else
    // Available bits in isa field are architecture-specific.
# error unknown architecture
# endif

// SUPPORT_NONPOINTER_ISA
#endif

};
複製代碼

SUPPORT_NONPOINTER_ISA 用於標記是否支持優化的 isa 指針,其字面含義意思是 isa 的內容再也不是類的指針了,而是包含了更多信息,好比引用計數,析構狀態,被其餘 weak 變量引用狀況。判斷方法也是根據設備類型:架構

// Define SUPPORT_NONPOINTER_ISA=1 to enable extra data in the isa field.
#if !__LP64__ || TARGET_OS_WIN32 || TARGET_IPHONE_SIMULATOR || __x86_64__
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
複製代碼

綜合看來目前只有 arm64 架構的設備支持,下面列出了 isa 指針中變量對應的含義:併發

變量名	含義
indexed	0 表示普通的 isa 指針,1 表示使用優化,存儲引用計數
has_assoc	表示該對象是否包含 associated object,若是沒有,則析構時會更快
has_cxx_dtor	表示該對象是否有 C++ 或 ARC 的析構函數,若是沒有,則析構時更快
shiftcls	類的指針
magic	固定值爲 0xd2,用於在調試時分辨對象是否未完成初始化。
weakly_referenced	表示該對象是否有過 weak 對象,若是沒有,則析構時更快
deallocating	表示該對象是否正在析構
has_sidetable_rc	表示該對象的引用計數值是否過大沒法存儲在 isa 指針
extra_rc	存儲引用計數值減一後的結果
複製代碼

在 64 位環境下,優化的 isa 指針並非就必定會存儲引用計數,畢竟用 19bit (iOS 系統)保存引用計數不必定夠。須要注意的是這 19 位保存的是引用計數的值減一。has_sidetable_rc 的值若是爲 1,那麼引用計數會存儲在一個叫 SideTable 的類的屬性中,後面會詳細講。app

散列表

散列表來存儲引用計數具體是用 DenseMap 類來實現,這個類中包含好多映射實例到其引用計數的鍵值對,並支持用 DenseMapIterator 迭代器快速查找遍歷這些鍵值對。接着說鍵值對的格式:鍵的類型爲 DisguisedPtr<objc_object>,DisguisedPtr 類是對 objc_object * 指針及其一些操做進行的封裝,目的就是爲了讓它給人看起來不會有內存泄露的樣子(真是心機裱),其內容能夠理解爲對象的內存地址;值的類型爲 __darwin_size_t,在 darwin 內核通常等同於 unsigned long。其實這裏保存的值也是等於引用計數減一。使用散列表保存引用計數的設計很好,即便出現故障致使對象的內存塊損壞,只要引用計數表沒有被破壞,依然能夠順藤摸瓜找到內存塊的位置。 以前說引用計數表是個散列表,這裏簡要說下散列的方法。有個專門處理鍵的 DenseMapInfo 結構體,它針對 DisguisedPtr 作了些優化匹配鍵值速度的方法:ide

struct DenseMapInfo<DisguisedPtr<T>> {
  static inline DisguisedPtr<T> getEmptyKey() {
    return DisguisedPtr<T>((T*)(uintptr_t)-1);
  }
  static inline DisguisedPtr<T> getTombstoneKey() {
    return DisguisedPtr<T>((T*)(uintptr_t)-2);
  }
  static unsigned getHashValue(const T *PtrVal) {
      return ptr_hash((uintptr_t)PtrVal);
  }
  static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
      return LHS == RHS; 
  }
};
複製代碼

固然這裏的哈希算法會根據是否爲 64 位平臺來進行優化,算法具體細節就不深究了,我總以爲蘋果在這裏的 hardcode 是隨便寫的:函數

#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
    key ^= key >> 4;
    key *= 0x5052acdb;
    key ^= __builtin_bswap32(key);
    return key;
}
#endif
複製代碼

再介紹下 SideTable 這個類,它用於管理引用計數表和 weak 表,並使用 spinlock_lock 自旋鎖來防止操做表結構時可能的競態條件。它用一個 64*128 大小的 uint8_t 靜態數組做爲 buffer 來保存全部的 SideTable 實例。並提供三個公有屬性:工具

spinlock_t slock;//保證原子操做的自選鎖
RefcountMap refcnts;//保存引用計數的散列表
weak_table_t weak_table;//保存 weak 引用的全局散列表
複製代碼

還提供了一個工廠方法,用於根據對象的地址在 buffer 中尋找對應的 SideTable 實例:

1 static SideTable *tableForPointer(const void *p)
複製代碼

weak 表的做用是在對象執行 dealloc 的時候將全部指向該對象的 weak 指針的值設爲 nil,避免懸空指針。這是 weak 表的結構:

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
複製代碼

蘋果使用一個全局的 weak 表來保存全部的 weak 引用。並將對象做爲鍵,weak_entry_t 做爲值。weak_entry_t 中保存了全部指向該對象的 weak 指針。

獲取引用計數

在非 ARC 環境可使用 retainCount 方法獲取某個對象的引用計數,其會調用 objc_object 的 rootRetainCount() 方法:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}
複製代碼

在 ARC 時代除了使用 Core Foundation 庫的 CFGetRetainCount() 方法,也可使用 Runtime 的 _objc_rootRetainCount(id obj) 方法來獲取引用計數,此時須要引入 <objc/runtime.h> 頭文件。這個函數也是調用 objc_object 的 rootRetainCount() 方法:

inline uintptr_t 
objc_object::rootRetainCount()
{
    assert(!UseGC);
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    if (bits.indexed) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
複製代碼

rootRetainCount() 方法對引用計數存儲邏輯進行了判斷,由於 TaggedPointer 前面已經說過了,能夠直接獲取引用計數;64 位環境優化的 isa 指針前面也說過了,因此這裏的重頭戲是在 TaggedPointer 沒法使用時調用的 sidetable_retainCount() 方法:

uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable *table = SideTable::tableForPointer(this);

    size_t refcnt_result = 1;
    
    spinlock_lock(&table->slock);
    RefcountMap::iterator it = table->refcnts.find(this);
    if (it != table->refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    spinlock_unlock(&table->slock);
    return refcnt_result;
}
複製代碼

sidetable_retainCount() 方法的邏輯就是先從 SideTable 的靜態方法獲取當前實例對應的 SideTable 對象,其 refcnts 屬性就是以前說的存儲引用計數的散列表,這裏將其類型簡寫爲 RefcountMap:

1 typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true>
RefcountMap;
複製代碼

而後在引用計數表中用迭代器查找當前實例對應的鍵值對,獲取引用計數值,並在此基礎上 +1 並將結果返回。這也就是爲何以前說引用計數表存儲的值爲實際引用計數減一。 須要注意的是爲何這裏把鍵值對的值作了向右移位操做(it->second >> SIDE_TABLE_RC_SHIFT):

#ifdef __LP64__
# define WORD_BITS 64
#else
# define WORD_BITS 32
#endif

// The order of these bits is important.
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)RefcountMap
複製代碼

能夠看出值的第一個 bit 表示該對象是否有過 weak 對象,若是沒有,在析構釋放內存時能夠更快;第二個 bit 表示該對象是否正在析構。從第三個 bit 開始纔是存儲引用計數數值的地方。因此這裏要作向右移兩位的操做,而對引用計數的 +1 和 -1 可使用 SIDE_TABLE_RC_ONE,還能夠用 SIDE_TABLE_RC_PINNED 來判斷是否引用計數值有可能溢出。 固然不可以徹底信任這個 _objc_rootRetainCount(id obj) 函數,對於已釋放的對象以及不正確的對象地址,有時也返回 「1」。它所返回的引用計數只是某個給定時間點上的值,該方法並未考慮到系統稍後會把自動釋放吃池清空,於是不會將後續的釋放操做從返回值裏減去。clang 會盡量把 NSString 實現成單例對象,其引用計數會很大。若是使用了 TaggedPointer,NSNumber 的內容有可能就再也不放到堆中,而是直接寫在寬敞的64位棧指針值裏。其看上去和真正的 NSNumber 對象同樣,只是使用 TaggedPointer 優化了下,但其引用計數可能不許確。

修改引用計數

retain 和 release

在非 ARC 環境下可使用 retain 和 release 方法對引用計數進行加一減一操做,它們分別調用了 _objc_rootRetain(id obj) 和 _objc_rootRelease(id obj) 函數,不事後二者在 ARC 環境下也可以使用。最後這兩個函數又會調用 objc_object 的下面兩個方法:

inline id 
objc_object::rootRetain()
{
    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

inline bool 
objc_object::rootRelease()
{
    assert(!UseGC);

    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}
複製代碼

這樣的實現跟獲取引用計數相似,先是看是否支持 TaggedPointer(畢竟數據存在棧指針而不是堆中,棧的管理原本就是自動的),不然去操做 SideTable 中的 refcnts 屬性,這與獲取引用計數策略相似。sidetable_retain() 將 引用計數加一後返回對象,sidetable_release() 返回是否要執行 dealloc 方法:

bool 
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable *table = SideTable::tableForPointer(this);

    bool do_dealloc = false;

    if (spinlock_trylock(&table->slock)) {
        RefcountMap::iterator it = table->refcnts.find(this);
        if (it == table->refcnts.end()) {
            do_dealloc = true;
            table->refcnts[this] = SIDE_TABLE_DEALLOCATING;
        } else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it. do_dealloc = true; it->second |= SIDE_TABLE_DEALLOCATING; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second -= SIDE_TABLE_RC_ONE; } spinlock_unlock(&table->slock); if (do_dealloc && performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return do_dealloc; } return sidetable_release_slow(table, performDealloc); } 複製代碼

看到這裏知道爲何在存儲引用計數時老是真正的引用計數值減一了吧。由於 release 原本是要將引用計數減一,因此存儲引用計數時先預留了個「一」,在減一以前先看看存儲的引用計數值是否爲 0 (it->second < SIDE_TABLE_DEALLOCATING),若是是,那就將對象標記爲「正在析構」(it->second |= SIDE_TABLE_DEALLOCATING),併發送 dealloc 消息,返回 YES;不然就將引用計數減一(it->second -= SIDE_TABLE_RC_ONE)。這樣作避免了負數的產生。 除此以外,Core Foundation 庫中也提供了增減引用計數的方法。好比在使用 Toll-Free Bridge 轉換時使用的 CFBridgingRetain 和 CFBridgingRelease 方法,其本質是使用 __bridge_retained 和 __bridge_transfer 告訴編譯器此處須要如何修改引用計數:

NS_INLINE CF_RETURNS_RETAINED CFTypeRef __nullable CFBridgingRetain(id __nullable X) {
    return (__bridge_retained CFTypeRef)X;
}

NS_INLINE id __nullable CFBridgingRelease(CFTypeRef CF_CONSUMED __nullable X) {
    return (__bridge_transfer id)X;
}
複製代碼

此外 Objective-C 不少實現是靠 Core Foundation Runtime 來實現, Objective-C Runtime 源碼中有些地方明確註明:」// Replaced by CF「,那就是意思說這塊任務被 Core Foundation 庫接管了。固然 Core Foundation 有一部分是開源的。還有一些 Objective-C Runtime 函數的實現被諸如 ObjectAlloc 和 NSZombie 這樣的內存管理工具所替代:

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}
複製代碼

alloc, new, copy, mutableCopy

根據編譯器的約定,這以這四個單詞開頭的方法都會使引用計數加一。而 new 至關於調用 alloc 後再調用 init:

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
+ (id)alloc {
    return _objc_rootAlloc(self);
}
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
複製代碼

能夠看出 alloc 和 new 最終都會調用 callAlloc,默認使用 Objective-C 2.0 且忽視垃圾回收和 NSZone,那麼後續的調用順序依次是爲:

class_createInstance()
_class_createInstanceFromZone()
calloc()
複製代碼

calloc() 函數相比於 malloc() 函數的優勢是它將分配的內存區域初始化爲0,至關於 malloc() 後再用 memset() 方法初始化一遍。 copy 和 mutableCopy 都是基於 NSCopying 和 NSMutableCopying 方法約定,分別調用各種本身實現的 copyWithZone: 和 mutableCopyWithZone: 方法。這些方法不管實現方式是深拷貝仍是淺拷貝,都會增長引用計數。(有些類的策略是懶拷貝,只增長引用計數但並不真的拷貝,等對象內容發生變化時再拷貝一份出來,好比 NSArray)。 在 retain 方法加符號斷點會發現 alloc, new, copy, mutableCopy 這四個方法都會經過 Core Foundation 的 CFBasicHashAddValue() 函數來調用 retain 方法。其實 CF 有個修改和查看引用計數的入口函數 __CFDoExternRefOperation,在 CFRuntime.c 文件中實現。

autorelease

本想貼上一堆 Runtime 中關於自動釋放池的源碼而後說上一大堆,而後發現了太陽神的這篇黑幕背後的Autorelease把我想說的都說了,把我不知道的也說了,簡直太屌了。 其實經過看源碼能夠知道好多細節,沒事點進去各類宏定義每每會獲得驚喜:哇,原來是這麼回事,XX 就是 XX 之類。。。

Reference

www.sealiesoftware.com/blog/archiv… www.opensource.apple.com

相關文章
相關標籤/搜索