天天看點

Objective-C 引用計數原理

引用計數如何存儲

有些對象如果支援使用

TaggedPointer

,蘋果會直接将其指針值作為引用計數傳回;如果目前裝置是 64 位環境并且使用 Objective-C 2.0,那麼“一些”對象會使用其

isa

指針的一部分空間來存儲它的引用計數;否則 Runtime 會使用一張散清單來管理引用計數。

其實還有一種情況會改變引用計數的存儲政策,那就是是否使用垃圾回收(用

UseGC

屬性判斷),但這種早已棄用的東西就不要管了,而且初始化垃圾回收機制的

void gc_init(BOOL wantsGC)

方法一直被傳入

NO

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

指針結構。

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

    Class cls;
    unintptr_t bits;

#if SUPPORT_NONPOINTER_ISA
# if __arme64__
#   define ISA_MASK        0x00000001fffffff8ULL
#   define ISA_MAGIC_MASK  0x000003fe00000001ULL
#   define ISA_MAGIC_VALUE 0x000001a400000001ULL
    struct {
        uintptr_t indexed           : ;
        uintptr_t has_assoc         : ;
        uintptr_t has_cxx_dtor      : ;
        uintptr_t shiftcls          : ; // MACH_VM_MAX_ADDRESS 0x1a0000000
        uintptr_t magic             : ;
        uintptr_t weakly_referenced : ;
        uintptr_t deallocating      : ;
        uintptr_t has_sidetable_rc  : ;
        uintptr_t extra_rc          : ;
#       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           : ;
        uintptr_t has_assoc         : ;
        uintptr_t has_cxx_dtor      : ;
        uintptr_t shiftcls          : ; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t weakly_referenced : ;
        uintptr_t deallocating      : ;
        uintptr_t has_sidetable_rc  : ;
        uintptr_t extra_rc          : ;
#       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

的類的屬性中,後面會講到。

散清單

散清單來存儲引用計數具體是用

DenseMap

類來實作,這個類中包含好多映射執行個體到其引用計數的鍵值對,并支援用

DenseMapIterator

疊代器快速查找周遊這些鍵值對。接着說鍵值對的格式:鍵的類型為

DisguisedPtr<objc_object>

DisguisedPtr

類是對

objc_object *

指針及其一些操作進行的封裝,目的就是為了讓它給人看起來不會有記憶體洩露的樣子(真是心機裱),其内容可以了解為對象的記憶體位址;值的類型為

__darwin_size_t

,在 darwin 核心一般等同于

unsigned long

。其實這裡儲存的值也是等于引用計數減一。使用散清單儲存引用計數的設計很好,即使出現故障導緻對象的記憶體塊損壞,隻要引用計數表沒有被破壞,依然可以順藤摸瓜找到記憶體塊的位置。

之前說引用計數表是個散清單,這裡簡要說下散列的方法。有個專門處理鍵的

DenseMapInfo

結構體,它針對

DisguisedPtr

做了些優化比對鍵值速度的方法:

struct DenseMapInfo<DisguisedPtr<T>> {
  static inline DisguisedPtr<T> getEmptyKey() {
    return DisguisedPtr<T>((T*)(uintptr_t)-);
  }
  static inline DisguisedPtr<T> getTombstoneKey() {
    return DisguisedPtr<T>((T*)(uintptr_t)-);
  }
  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 >> ;
    key *= ;
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
    key ^= key >> ;
    key *= ;
    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

執行個體:

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 =  + 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 = ;

    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

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 之類。。。

本人注:

Autorelease

對象是在目前的

runloop

疊代結束時釋放的,而釋放的原因是系統在每個 runloop 疊代中都加入了自動釋放池的 Push 和 Pop

Reference

http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html

http://www.opensource.apple.com

本文轉載自:http://yulingtianxia.com/blog/2015/12/06/The-Principle-of-Refenrence-Counting/

本文總結:總的來說,Objective-C 中的引用計數是通過

isa

指針的結構體維護的。引用計數會存在優化過(

TaggedPointer

技術)的

isa

指針位址或者在

SideTable

(散清單)的類中,再通過對外提供一些方法來操作其數值。