objc源碼解析|引用計數(shù)管理

通過以下方法查看iOS的引用計數(shù)管理:

  • alloc
  • retain
  • release
  • retainCount
  • dealloc

源碼版本:objc4-723

alloc


+ (id)alloc {
    return _objc_rootAlloc(self);
}

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

當(dāng)我們調(diào)用類的alloc方法時,調(diào)用的方法棧如上所示,我們來看最后一部分代碼即可。

在這一部分代碼中,有兩個宏定義:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

__builtin_expect的主要作用就是幫助編譯器判斷條件跳轉(zhuǎn)的預(yù)期值,避免因執(zhí)行jmp跳轉(zhuǎn)指令造成時間浪費(fèi)。

編譯器優(yōu)化時,根據(jù)條件跳轉(zhuǎn)的預(yù)期值,按正確地順序生成匯編代碼,把“很有可能發(fā)生”的條件分支放在順序執(zhí)行指令段,而不是jmp指令段(jmp指令會打亂CPU的指令執(zhí)行順序,大大影響CPU指令執(zhí)行效率)。
在本例中,if else句型編譯后, 一個分支的匯編代碼緊隨前面的代碼,而另一個分支的匯編代碼需要使用jmp指令才能訪問到,很明顯通過jmp訪問需要更多的時間, 在復(fù)雜的程序中,有很多的if else句型,又或者是一個有if else句型的庫函數(shù),每秒鐘被調(diào)用幾萬次,通常程序員在分支預(yù)測方面做得很糟糕,編譯器又不能精準(zhǔn)的預(yù)測每一個分支,這時jmp產(chǎn)生的時間浪費(fèi)就會很大,函數(shù)__builtin_expert()就是用來解決這個問題的。

所以,這里的邏輯就是如果slowpath(checkNil && !cls)0時,該函數(shù)返回nil,為1時走接下里的邏輯。

在接下來的邏輯中,fastpath(!cls->ISA()->hasCustomAWZ()首先會判斷當(dāng)前classsuper class是否實現(xiàn)了allocWithZone:方法(AWZAllocWithZone),如果沒有實現(xiàn)這個方法,最終都會調(diào)用C語言函數(shù)calloc申請一塊內(nèi)存空間。

如果實現(xiàn)了AWZ方法,就會執(zhí)行allocWithZone:。

總結(jié),調(diào)用alloc時,經(jīng)過一系列方法調(diào)用,最終會在內(nèi)存中申請一塊內(nèi)存空間,但此時并沒有設(shè)置引用計數(shù)。

retain


- (id)retain {
    return ((id)self)->rootRetain();
}


ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    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;
}

在上面代碼塊中,因為篇幅所限省去了rootRetain()的具體實現(xiàn)代碼,在rootRetain()中實際上調(diào)用了sidetable_retain()方法,也就是最后那部分代碼。

在了解最后一部分代碼前,需要了解一些概念上的知識,有助于更好的理解系統(tǒng)對對象引用計數(shù)管理的實現(xiàn)原理。

為了管理所有對象的引用計數(shù)以及對象的所有弱引用指針,系統(tǒng)創(chuàng)建了一個全局的SideTables,SideTables里存的是一個個的SideTable,每個SideTable實際上都是一個結(jié)構(gòu)體,如下:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    //省略部分代碼

SideTables本質(zhì)上是一個全局的hash表,key值為對象的內(nèi)存地址。
回到sidetable_retain()方法,首先會根據(jù)當(dāng)前對象的指針到SideTables中獲取SideTable

SideTable& table = SideTables()[this];

然后在SideTable的結(jié)構(gòu)體當(dāng)中獲取當(dāng)前對象的引用計數(shù)值:

size_t &refcntStorage = table.refcnts[this];

需要注意的是,這兩次查找都是hash查找。
然后再對應(yīng)用計數(shù)值進(jìn)行+1操作:

refcntStorage += SIDE_TABLE_RC_ONE;

以上,就是進(jìn)行retain操作時,系統(tǒng)對對象引用計數(shù)操作的具體實現(xiàn)。

release


- (oneway void)release {
    ((id)self)->rootRelease();
}

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    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;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

篇幅所限,省略了rootRelease()的代碼實現(xiàn),在rootRelease()中會調(diào)用sidetable_release(),也就是最后一部分代碼,我們可以將最后一部分代碼進(jìn)行簡化,簡化后的代碼如下所示:

SideTable &table = SideTables()[this];
RefcountMap ::iterator it = table.refcnts.find(this);
it->second -= SIDE_TABLE_RC_ONE;

首先根據(jù)對象的內(nèi)存地址到hash表中查找SideTable

SideTable &table = SideTables()[this];

然后根據(jù)查找到的SideTable和對象的內(nèi)存地址獲取其引用計數(shù)并-1

RefcountMap ::iterator it = table.refcnts.find(this);
it->second -= SIDE_TABLE_RC_ONE;

另外,當(dāng)對象需要被回收時,系統(tǒng)還會利用objc_msgSend向?qū)ο蟀l(fā)送dealloc消息。

以上,就是進(jìn)行release操作時,系統(tǒng)對對象引用計數(shù)操作的具體實現(xiàn)。

retainCount


- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}


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

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        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();
}

uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    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;
    }
    table.unlock();
    return refcnt_result;
}

這里把retainCount方法調(diào)用棧的全部代碼都貼出來了,主要來看最后一部分代碼,也就是sidetable_retainCount()方法的代碼。

我們也可以帶著一個問題來看這段代碼,問題是:

為什么剛創(chuàng)建完的對象,它的retainCount0?

在這個方法的實現(xiàn)中,首先會根據(jù)對象的內(nèi)存地址獲取到SideTable

SideTable& table = SideTables()[this];

然后緊接著聲明了一個局部變量refcnt_result,值為1

size_t refcnt_result = 1;

所以,剛alloc出來的對象,在引用計數(shù)表(SideTables)中是沒有這個對象的key-value映射的,所以table.refcnts.find(this)讀出來的值是0,所以如果這里是0,就將局部變量refcnt_result的值返回,也即是1。

如果table.refcnts.find(this)讀出來的值不是0,則會把查找的結(jié)果做向右偏移的操作然后+1,返回給調(diào)用方:

refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;

以上,就是進(jìn)行retainCount操作時,系統(tǒng)對獲取對象引用計數(shù)的具體實現(xiàn)原理。

dealloc


- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

以上是dealloc的方法調(diào)用棧,從最后一個方法開始看,在這個方法中有一個if判斷條件比較多:

  • 首先判斷有沒有使用非指針型isanonpointer_isa
  • 判斷是否有weak指針指向它
  • 判斷是否有關(guān)聯(lián)對象
  • 判斷內(nèi)部實現(xiàn)是否涉及到有C++相關(guān)的內(nèi)容
  • 當(dāng)前對象的引用計數(shù)是否是通過SideTable來維護(hù)的

只有當(dāng)當(dāng)前對象既不是nonpointer_isa,也沒有弱引用,還沒有涉及到C++、關(guān)聯(lián)對象、沒有使用SideTable存儲相關(guān)引用計數(shù),才會調(diào)用C函數(shù)free()進(jìn)行釋放,否則,就調(diào)用object_dispose()對象清除函數(shù)進(jìn)行清除。

再來看object_dispose()函數(shù)的實現(xiàn):

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}


void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

先來看objc_destructInstance函數(shù),如果obj對象存在的話,會先判斷當(dāng)前對象是否有用到涉及到C++相關(guān)的東西,如果有,稍后會調(diào)用object_cxxDestruct函數(shù)進(jìn)行銷毀操作。

還會判斷當(dāng)前對象是否擁有關(guān)聯(lián)對象,如果有的話稍后就會調(diào)用_object_remove_assocations函數(shù)進(jìn)行清除操作,所以我們并不需要在dealloc方法中顯式的清除關(guān)聯(lián)對象。

最后會調(diào)用objclearDeallocating函數(shù),函數(shù)實現(xiàn)如下:

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

其會調(diào)用兩個函數(shù),分別是:

  • sidetable_clearDeallocating()
  • clearDeallocating_slow()

(1)sidetable_clearDeallocating()函數(shù)實現(xiàn)如下:

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

其中還會調(diào)用weak_clear_no_lock()函數(shù),實現(xiàn)如下:

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

weak_clear_no_lock函數(shù)有兩個參數(shù),weak_tablereferent_id

weak_table就是弱引用表,記錄了指向該對象的弱引用指針。

referent_id就是當(dāng)前正在被銷毀的對象。

這里會把referent_id強(qiáng)轉(zhuǎn)成objc_object類型的結(jié)構(gòu)體指針,記做referent,根據(jù)referentweak_table中獲取entry,進(jìn)而通過weak_entry_t獲取弱引用數(shù)組referrers,繞后遍歷這個referrers數(shù)組,將指向該對象的弱引用指針全部置為nil,最后從weak_table中把這個entry刪除。

所以,當(dāng)一個對象有一個弱引用指針指向它的時候,當(dāng)這個對象被廢棄之后它的弱引用指針會被自動置為nil。

(2)clearDeallocating_slow()函數(shù)實現(xiàn)如下:

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

table.refcnts.erase(this);函數(shù)的主要作用是從引用計數(shù)表中擦除該對象的引用計數(shù)。

總結(jié)一下,當(dāng)對象被釋放時,也就是在系統(tǒng)的dealloc方法實現(xiàn)中,系統(tǒng)會銷毀該對象所使用的C++相關(guān)的內(nèi)容,還會刪除該對象的關(guān)聯(lián)對象,并且將指向該對象的弱引用指針全部置為nil,這些操作完成后,會從全局的引用計數(shù)表中擦除該對象的引用計數(shù)。

以上,就是dealloc的實現(xiàn)原理。

內(nèi)存管理方案


分類

  • TaggedPointer
    針對于小對象,如NSNumber、NSDate等。
  • NONPOINTER_ISA
    對于64位架構(gòu)下的應(yīng)用程序采用這種內(nèi)存管理方案,在64位架構(gòu)下,ISA指針占64個bit位,實際上有32位或40位就夠用了,剩余的是浪費(fèi)的,Apple為了提高內(nèi)存的利用率,在剩余的bit位當(dāng)中存儲一些關(guān)于內(nèi)存管理的數(shù)據(jù)內(nèi)容,也被稱為非指針型的isa。
  • 散列表
    包括引用計數(shù)表和弱引用表。

NONPOINTER_ISA

arm64架構(gòu)
arm64架構(gòu)
  • indexed
    0:代表它是一個純的isa指針,里面的內(nèi)容直接代表當(dāng)前對象的類對象的地址。
    1:代表isa指針里面存儲的不僅是類對象的地址,還有一些內(nèi)存管理的數(shù)據(jù)。
  • has_assoc
    當(dāng)前對象是否有關(guān)聯(lián)對象
    0:沒有
    1:有
  • has_cxx_dtor
    當(dāng)前對象是否有使用到C++語言方面的內(nèi)容
  • 后續(xù)33位
    當(dāng)前對象的類對象的指針地址
  • magic
    略,不影響分析
  • weakly_referenced
    當(dāng)前對象是否有弱引用指針
  • deallocating
    表示當(dāng)前對象是否在進(jìn)行dealloc操作
  • has_sidetable_rc
    指當(dāng)前isa指針當(dāng)中如果所存儲的引用計數(shù)已經(jīng)達(dá)到了上限的話,需要外掛一個sidetable這樣的數(shù)據(jù)結(jié)構(gòu)去存儲相關(guān)的引用計數(shù)內(nèi)容。
  • extra_rc
    額外的引用計數(shù),當(dāng)引用計數(shù)在一個很小的值得范圍內(nèi)就會存到isa指針當(dāng)中,而不是由單獨(dú)的引用計數(shù)表去存儲引用計數(shù),

散列表方式

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

在非嵌入式系統(tǒng)中,SideTables管理著64個SideTable,每個SideTable有3個元素:

  • spinlock_t自旋鎖
  • RefcountMap引用計數(shù)表
  • weak_table_t弱引用表

為什么不是一個SideTable,而是由多個SideTable組成SideTables

如果使用一個SideTable管理系統(tǒng)所有對象的引用計數(shù),那么當(dāng)我們在多線程中對其中一個對象進(jìn)行retainrelease等操作時就會對這個SideTable加鎖,以此保證數(shù)據(jù)訪問的安全,那么在這個過程中實際上就產(chǎn)生了效率的問題,下一個對象要想進(jìn)行操作(比如修改引用計數(shù)),就必須要等鎖釋放只會才能操作這張表,如果成千上萬個對象同操作一張表,那么效率是極其低下的。

系統(tǒng)為了解決效率低下的問題,系統(tǒng)引入了分離鎖的技術(shù)方案,我們可以把對象所對應(yīng)的引用計數(shù)表可以分拆成多個部分,比如可以分拆成8個,那么就會對8個表分別加鎖,假如A對象屬于表1,B對象屬于表2,那么此時就可以進(jìn)行并行操作,提高了訪問效率。

如何實現(xiàn)快速分流?

指通過一個對象的指針,如何快速定位到它屬于哪張SideTable?

SideTable的本質(zhì)是一張Hash表,這張Hash表中可能有64張具體的SideTable用以存儲不同對象的引用計數(shù)表和弱引用表。

Hash表的key是對象指針經(jīng)過hash函數(shù)計算出一個值,這個值決定對象所屬的SideTable是哪一個,或者說在數(shù)組中(SideTables)的位置是第幾個。

Hash查找

eg:給定值是對象的內(nèi)存地址,目標(biāo)值是數(shù)組(SideTables)下標(biāo)索引。

f(ptr) = (uintptr_t)ptr % array.count

其他涉及到的技術(shù)


  • spinlock_t slock;自旋鎖
  • RefcountMap refcnts;引用計數(shù)表
  • weak_table_t weak_table;弱引用表

spinlock_t

  • 自旋鎖是一種忙等的鎖,指當(dāng)前鎖已經(jīng)被其他線程獲取,當(dāng)前線程會不斷探測這個鎖是否有被釋放,如果被釋放掉自己會第一時間獲取這個鎖。
    • 信號量是如果獲取不到這個鎖的時候,會把自己線程進(jìn)行阻塞休眠,等到其他線程釋放這個鎖的時候喚醒當(dāng)前線程。
  • 適用于輕量訪問,因為對于引用計數(shù)的操作只是簡單的+1 -1操作,這種操作都是輕量的操作,所以在輕量的訪問場景下,都可以使用自旋鎖。

RefcountMap

實際上是一個Hash表,可以通過對象的指針查找到對應(yīng)的引用計數(shù),查找的過程也是哈希查找,提高查找效率。

  • size_t共有64位
    • 第一位是weakly_referenced,表示對象是否有弱引用,0就是沒有,1就是有。
    • 第二位是deallocating,表示當(dāng)前對象是否正在dealloc。
    • 其余位存儲的就是對象的實際引用計數(shù)值,具體的引用計數(shù)值實際上要偏移兩位,因為前面有兩位是weakly_referenceddeallocating,我們要把這兩位去掉。

weak_table_t

實際上也是一張Hash表,key為對象指針,valueweak_entry_t,weak_entry_t是一個結(jié)構(gòu)體數(shù)組,數(shù)組存儲的每一個對象就是弱引用指針,也就是在代碼中定義的__weak變量,這個變量的指針就存儲在weak_entry_t中。

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容