iOS原理 引用計數

iOS原理 文章匯總

前言

在iOS中,對象的內存是通過引用計數(Reference Count)來管理的速客。每當有一個新的強引用指針指向偿洁,對象的引用計數就會+1,當減少一個強引用指針轿钠,引用計數就會-1巢钓,當引用計數為0時,對象就會被銷毀疗垛。

一症汹、引用計數值的存儲

在前面介紹alloc核心步驟的相關文章中有提到,一個nonpointer類型的對象贷腕,它的引用計數是存放在成員isa_t里的extra_rc中背镇。在__arm64__環(huán)境下咬展,extra_rc在內存中占19位,在__x86_64__環(huán)境下瞒斩,占8位破婆。

isa_t結構里和內存管理相關的成員除extra_rc外,還有weakly_referenced济瓢、has_sidetable_rc以及deallocating這三個荠割,具體情況可參考iOS原理 alloc核心步驟3:initInstanceIsa詳解

__x86_64__環(huán)境為例旺矾,extra_rc大小總共8bit蔑鹦,最多存放2^7量級的數值。因此如果只用extra_rc來存儲引用計數值箕宙,就會遇到下面3個問題:

  • extra_rc達到最大值嚎朽,此時對象又被一個新的強引用指針指向,該如何處理柬帕?
  • 若對象不是nonpointer類型哟忍,則isa_t結構里就沒有extra_rc成員,此時引用計數該如何處理陷寝?
  • 若對象被一個弱引用指針指向锅很,該如何處理?

基于此凤跑,除了extra_rc外爆安,OC中還使用了SideTables散列表來管理引用計數。

二仔引、SideTables 散列表

SideTables是一個全局的哈希數組扔仓,里面存儲了多張SideTable。本質是一個StripedMap結構體咖耘,內部成員StripeCount表示SideTable的最大數量:

#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

可以看到翘簇,在iOS的真機模式下,SideTable最多為8張儿倒,在MacOS或者模擬器模式下版保,最多為64張。

SideTables的哈希key就是對象的地址义桂,每個地址都會映射一張SideTable找筝,由于最大數量限制,因此會有很多對象地址映射同一張SideTable慷吊。通過對哈希函數傳入對象地址袖裕,即可得到對應的SideTable

2.1 SideTable

SideTable里面主要存放了對象的引用計數和弱引用相關信息溉瓶,結構如下:

struct SideTable {
    
    //成員
    spinlock_t slock;          //自旋鎖急鳄,防止多線程訪問沖突
    RefcountMap refcnts;       //引用計數表
    weak_table_t weak_table;   //弱引用表

    //函數
    ...  ...
};

內部有三個成員:

  • spinlock_t slock: 自旋鎖谤民,用于上鎖/解鎖SideTable

    spinlock_t實質上是一個uint32_t類型的非公平的自旋鎖(unfair lock),當值大于0時疾宏,鎖可用张足,當等于或小于0時,需要鎖等待坎藐。在_os_unfair_lock_opaque中記錄了獲取鎖的線程信息为牍,只有獲得該鎖的線程才能夠解開這把鎖。

    非公平:指獲取鎖的順序和申請鎖的順序無關岩馍。也就是說碉咆,第一個申請鎖的線程有可能最后是最后一個獲取鎖,或者剛獲得鎖的線程也有可能會再次立刻獲取鎖蛀恩,造成饑餓等待疫铜。

  • RefcountMap refcnts:引用計數表,存儲對象的引用計數

    RefcountMap 實質上是一個以objc_object為key的hash表双谆,value為對象的引用計數壳咕。RefcountMap中可以存儲多個對象的引用計數,因此一個SideTable會對應多個對象顽馋。對于nonpointer類型對象谓厘,當extra_rc達到最大值后,才會在RefcountMap中存放引用計數寸谜,而對于非nonpointer類型對象庞呕,直接在里面存放引用計數。當對象的引用計數變?yōu)?時程帕,會自動將相關的信息從hash表中刪除。

  • weak_table_t weak_table:弱引用表地啰,存儲對象的弱引用指針

    weak_table_t也是一個以objc_object為key的hash表愁拭,結構如下:

    struct weak_table_t {
        weak_entry_t *weak_entries;        // hash數組,用來存儲弱引用對象的相關信息
        size_t    num_entries;             // hash數組中的元素個數
        uintptr_t mask;                    // hash數組長度-1亏吝,會參與hash計算岭埠。(注意,這里是hash數組的長度蔚鸥,而不是元素個數惜论。比如,數組長度可能是64止喷,而元素個數僅存了2個)
        uintptr_t max_hash_displacement;   // 可能會發(fā)生的hash沖突的最大次數馆类,用于判斷是否出現了邏輯錯誤(hash表中的沖突次數絕不會超過改值)
    };
    

    每個對象對應一個weak_entry_t,其結構如下:

     struct weak_entry_t {
         //被引用的對象
         DisguisedPtr<objc_object> referent;   
         //存儲該對象的弱引用指針
         //如果弱引用指針數量大于4弹谁,存放在referrers數組乾巧,小于4句喜,存放在inline_referrers數組
         union {
             struct {
                 weak_referrer_t *referrers;   // 存儲弱引用指針地址的hash數組
                 uintptr_t        out_of_line_ness : 2;
                 uintptr_t        num_refs : PTR_MINUS_2;
                 uintptr_t        mask;
                 uintptr_t        max_hash_displacement;
              };
             struct {
                 // out_of_line_ness field is low bits of inline_referrers[1]
                 weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];   // 存儲弱引用指針地址的hash數組
             };
         };
    
         //函數
         ...  ...
     };
    

    由此可知,對象的弱引用指針都存儲在其對應的weak_entry_t里的數組中沟于。當對象被一個新的弱引用指針指向時咳胃,就會往數組里添加這個指針,若指針指向nil或者其它對象旷太,則將該指針從數組里移除展懈,若對象的引用計數為0,則會將數組里的所有弱引用指針指向nil供璧,再移除存崖。

2.2 SideTable存在多張的原因
  • 若全局只用一張SideTable來管理所有對象,每次訪問一個對象都會進行一次開/解鎖操作嗜傅,訪問其他對象需要等待金句,效率過低。
  • 若為每個對象都建立一個SideTable吕嘀,則會造成內存浪費违寞,耗費性能。
  • 至于為什么最多為8張或者64張偶房,目前并沒有什么數據模型和理論支撐趁曼,猜測是設計SideTables的作者根據經驗選擇的一個定值。

三棕洋、引用計數的底層處理

MRC中挡闰,需要程序員手動調用retain方法來使引用計數+1,調用release方法來使引用計數-1掰盘,當引用計數為0時摄悯,會調用dealloc方法銷毀。在ARC中也一樣愧捕,只不過不需要程序員手動調用奢驯,編譯器會自動調用。

3.1 retain 源碼分析

在源碼中retain操作的底層函數調用鏈為objc_retain -> retain -> rootRetain次绘,最終實現代碼如下:

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    //1.若對象為TaggedPointer對象瘪阁,直接返回
    if (isTaggedPointer()) return (id)this;
    
    //聲明兩個標記
    bool sideTableLocked = false;    //sideTable是否被鎖
    bool transcribeToSideTable = false;   //是否需要更新SideTable中的引用計數

    //聲明兩個isa_t的局部變量,用于新舊值的替換
    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        //這里的isa是對象自身的isa邮偎,并賦值給兩個局部isa保存
        oldisa = LoadExclusive(&isa.bits);  
        newisa = oldisa;
        //2.若對象不是nonpointer類型管跺,直接操作sidetable
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            //若是元類對象,則直接返回
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //3.如果當前對象正在釋放禾进,執(zhí)行dealloc流程
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }

        //4.若對象是nonpointer類型的對象豁跑,則將extra_rc值+1
        //先通過左移運算獲取到isa里的extra_rc,+1后再將新值賦給isa
        //carry標記泻云,用來表示extra_rc的值是否已溢出
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        //判斷extra_rc值是否已溢出
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            //若溢出贩绕,則需要SideTable來儲存
            //更新上面聲明的SideTable的兩個標記
            sideTableLocked = true;
            transcribeToSideTable = true;
            //將(extra_rc最大值  + 1)的一半存儲在extra_rc中 
            //在__x86_64__下火的,extra_rc占8位,RC_HALF為1<<7淑倾,所以是(最大值 + 1)的一半
            newisa.extra_rc = RC_HALF;
            //將isa中的has_sidetable_rc值設為1馏鹤,表示該對象已經使用Sidetable來存儲引用計數了
            newisa.has_sidetable_rc = true;
        }
      //這個while判斷條件里面已經將newisa賦值給對象的isa了
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    //4.判斷是否需要更新SideTable里的引用計數
    //只在extra_rc達到最大值時,才需要更新
    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        //將(extra_rc最大值 + 1)的1/2存儲在Sidetable中
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    //解鎖SideTable
    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

經過源碼分析可知娇哆,retain的實現邏輯如下:

  • Step1:如果對象為TaggedPointer對象湃累,則直接返回,不做其他操作碍讨。
  • Step2:如果對象不是nonpointer類型治力,若對象為元類對象,則直接返回勃黍,若不是元類對象宵统,則直接操作SideTable,將對象的引用計數+1并保存在RefcountMap中覆获。
  • Step3:如果對象是否正在釋放马澈,則執(zhí)行dealloc流程。
  • Step4:如果對象是nonpointer類型弄息,執(zhí)行extra_rc+1痊班,并判斷extra_rc是否溢出。若已溢出摹量,則將(extra_rc最大值 + 1)的一半分別存在 isaextra_rcSideTableRefcountMap中涤伐。
  • 為什么是(extra_rc最大值 + 1)的一半?

    x86_64環(huán)境下缨称,extra_rc占8位凝果,最大值為255,此時再ratain一次睦尽,引用計數為256豆村,就溢出了,需要SideTable來存儲骂删。RC_HALF = 1<<7,值為128四啰,所以是(extra_rc最大值 + 1)的一半宁玫。

  • 為什么要將(extra_rc最大值 + 1)的一半分別存儲在extra_rcSideTable中?

    因為每次操作SideTable都需要進行一次上鎖/解鎖柑晒,而且還要經過幾次哈希運算才能處理對象的引用計數欧瘪,效率比較低。而且匙赞,考慮到release操作佛掖,也不能在溢出時把值全部存在SideTable中妖碉。因此,為了盡可能多的去操作extra_rc芥被,每當extra_rc溢出時欧宜,就各存一半,這樣下次進來就還是直接操作extra_rc拴魄,會更高效冗茸。

3.2 release 源碼分析

releaseretain的實現邏輯大體相同,只是將引用計數+1變?yōu)?code>-1匹中。在源碼中release操作的底層函數調用鏈為objc_release -> release -> rootRelease夏漱,最終實現代碼如下:

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    //1.若對象為TaggedPointer對象,直接返回
    if (isTaggedPointer()) return false;

    //聲明一個標記:sideTable是否被鎖
    bool sideTableLocked = false;

    //聲明兩個isa_t的局部變量顶捷,用于新舊值的替換
    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        //將對象的isa的賦值給兩個局部isa保存
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //2.若對象不是nonpointer類型挂绰,直接操作sidetable
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            //若是元類對象,則直接返回
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }

        // don't check newisa.fast_rr; we already called any RR overrides
       
        //3.若對象是nonpointer類型的對象服赎,則將extra_rc值-1
        //先通過左移運算獲取到isa里的extra_rc葵蒂,-1后再將新值賦給isa
        //carry標記,這里用來表示extra_rc的值是否為0
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            //若extra_rc的值為0专肪,進入underflow
            goto underflow;
        }
      //這個while判斷條件里面已經將newisa賦值給對象的isa了
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    //若extra_rc的值大于0刹勃,則解鎖SideTable,并返回
    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 //若extra_rc的值為0嚎尤,會跳來這里執(zhí)行
 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    //上面這句注釋表示這里的處理主要是從SideTable借引用計數或者直接釋放對象

    // abandon newisa to undo the decrement
    newisa = oldisa;
    
    //4.判斷對象是否已使用SideTable存儲引用計數
    //isa的has_sidetable_rc值為1荔仁,表示對象已使用SideTable儲引用計數
    if (slowpath(newisa.has_sidetable_rc)) {
        //容錯處理
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        //容錯處理
        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.    
        //取出SideTable中存儲的當前對象的引用計數值的一半,賦值給borrowed   
       //這一步操作后芽死,SideTable中存儲的值就只剩一半了
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        //判斷borrowed是否大于0
        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            //borrowed大于0乏梁,表示SideTable中還存有引用計數,所以不能釋放
           //borrowed - 1关贵,再把值賦給extra_rc限寞,下次又可以直接操作extra_rc
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
           //更新isa的值
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);

            //容錯處理,如果extra_rc賦值失敗怀薛,則再嘗試賦值一次
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            //容錯處理悯许,如果extra_rc賦值一直失敗,則將之前取出的一半引用計數值還給Sidetable
            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
           //解鎖SideTable并返回
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
            //borrowed等于0炭剪,表示對象的引用計數也為0练链,就走后面的dealloc流程
        }
    }

    // Really deallocate.
    //5.釋放對象
    //isa的has_sidetable_rc為0,說明對象沒有使用SideTable存儲引用計數奴拦,而此時extra_rc也為0媒鼓,即對象的引用計數為0,就直接釋放。
    if (slowpath(newisa.deallocating)) {
        //若當前對象正在釋放绿鸣,則不再執(zhí)行釋放操作疚沐,直接解鎖SideTable,并返回一個過度釋放的錯誤
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    //將isa的deallocating賦值為1潮模,表示正在執(zhí)行釋放操作
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

   //解鎖SideTable
    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    //發(fā)送一個dealloc消息亮蛔,釋放對象
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

經過源碼分析可知,release的實現邏輯如下:

  • Step1:如果對象為TaggedPointer對象再登,則直接返回尔邓,不做其他操作。
  • Step2:如果對象不是nonpointer類型锉矢,若對象為元類對象梯嗽,則直接返回,若不是元類對象沽损,則直接操作SideTable灯节,將對象的引用計數-1并保存在RefcountMap中,當引用計數變?yōu)?绵估,則釋放對象炎疆。
  • Step3:如果對象是nonpointer類型,執(zhí)行extra_rc-1国裳,判斷值是否為0形入。若不為0,更新isa的值并返回缝左;若為0亿遂,則判斷是否已經使用SideTable存儲對象的引用計數。
  • Step4:若isa.has_sidetable_rcw==1渺杉,表示已經使用SideTable存儲對象的引用計數 蛇数。則取出SideTable存儲的一半引用計數值,并判斷這一半值是否大于0是越,若大于0耳舅,則將(一半值 - 1)賦值給extra_rc,若等于0倚评,表示對象的引用計數為0浦徊,則釋放對象。
  • Step5:若isa.has_sidetable_rcw==0天梧,表示沒有使用SideTable存儲對象的引用計數盔性。此時對象的引用計數為0,就直接釋放對象腿倚。若當前正在釋放,則不再執(zhí)行新的釋放操作,并返回一個過度釋放的錯誤敷燎。

注意:從SideTable中取出一半引用計數值后暂筝,SideTable中存儲的值也只剩下一半,如果后續(xù)extra_rc的賦值失敗硬贯,再將取出的一半值還給SideTable焕襟。

sidetable_subExtraRC_nolock(RC_HALF)函數的實現中,有一步it->second = newRefcnt饭豹,就是將計算后的一半值存儲在SideTable中鸵赖。

3.3 dealloc 源碼分析

dealloc的邏輯就相對簡單點,在源碼中查看rootDealloc函數的實現如下:

inline void
objc_object::rootDealloc()
{
    
    //1.若對象為TaggedPointer對象拄衰,直接返回
    //(吐槽一下它褪,蘋果的人員都不確定這步判斷是否必要)
    if (isTaggedPointer()) return;  // fixme necessary?

    /**2.若對象為nonpointer類型,并且
     *沒有被弱引用
     *沒有關聯對象
     *沒有C++析構器
     *沒有使用SideTable存儲引用計數
     *就直接釋放內存空間   
     */
    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
        object_dispose((id)this);
    }
}

//3.清空對象的相關信息茫打,并釋放內存空間
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.
        //判斷是否有C++析構器
        bool cxx = obj->hasCxxDtor();
        //判斷是否有關聯對象
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //調用C++析構函數
        if (cxx) object_cxxDestruct(obj);
        //刪除關聯對象
        if (assoc) _object_remove_assocations(obj);
        //釋放
        obj->clearDeallocating();
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    //若對象不是nonpointer類型,則直接在SideTable中清空對象的相關信息
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        //這一步直接清空SideTable中對象的所有信息妖混,包括引用計數和弱引用指針
        sidetable_clearDeallocating();
    }
    //若對象是nonpointer類型老赤,并且在SideTable中存儲了弱引用指針或者引用計數
    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());
}

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    
    //獲取當前對象對應的SideTable
    SideTable& table = SideTables()[this];
    //上鎖
    table.lock();
    //清空弱引用指針
    if (isa.weakly_referenced) {
        //將弱引用表中當前對象關聯的所有指針都設為nil并移除
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //清空引用計數
    if (isa.has_sidetable_rc) {
        //從引用計數表中移除當前對象的引用計數
        table.refcnts.erase(this);
    }
    //解鎖
    table.unlock();
}

經過源碼分析可知,dealloc的實現邏輯如下:

  • Step1:如果對象為TaggedPointer對象制市,則直接返回抬旺,不做其他操作。
  • Step2:如果對象是nonpointer類型祥楣,且沒有被弱引用开财,沒有關聯對象,沒有C++析構器荣堰,沒有使用SideTable存儲引用計數床未,則直接釋放對象的內存空間。
  • Step3:如果對象有析構器振坚,則執(zhí)行C++析構函數薇搁。
  • Step4:如果對象有關聯對象,則刪除關聯對象渡八。
  • Step5:如果對象有弱引用指針啃洋,則將弱引用表中當前對象關聯的所有指針都設為nil并移除。
  • Step6:如果對象有在SideTable中存儲引用計數屎鳍,則將引用計數從表中移除宏娄。
  • Step7:若對象不是nonpointer類型,則直接在SideTable中清空對象的相關信息逮壁,包括引用計數和弱引用指針孵坚。
  • Step8:執(zhí)行free函數,釋放對象的內存空間。

四卖宠、獲取對象的引用計數 -- retainCount()

iOS中巍杈,獲取對象的引用計數有兩種方式:

  • 方式1:使用KVC獲取。
[obj valueForKey:@"retainCount"];
  • 方式2:使用CFGetRetainCount函數獲取扛伍。
CFGetRetainCount((__bridge CFTypeRef)(obj));

這兩個方式在源碼工程中通過斷點調式可知筷畦,都是調用retainCount函數來獲取對象的引用計數,查看函數調用鏈retainCount -> _objc_rootRetainCount -> rootRetainCount刺洒,最終實現如下:

inline uintptr_t 
objc_object::rootRetainCount()
{
     //1.若對象為TaggedPointer對象鳖宾,直接返回當前對象
    if (isTaggedPointer()) return (uintptr_t)this;
   
    sidetable_lock();
    //獲取isa中的數據
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //2.若對象為nonpointer類型,返回的引用計數為(extra_rc  + SideTable_rc + 1)
    if (bits.nonpointer) {
        //當前引用計數為(extra_rc + 1)
        uintptr_t rc = 1 + bits.extra_rc
        //若SideTable中存儲了對象的引用計數逆航,還需要加上這個引用計數值
        if (bits.has_sidetable_rc) {
            //注意:這一步加上的是SideTable里存儲的真實值鼎文,沒有+1操作
            //詳情查看拓展2
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        //返回引用計數
        return rc;
    }

    sidetable_unlock();
    //3.若對象不是nonpointer類型,返回(SideTable_rc + 1)
    //詳情查看拓展3
    return sidetable_retainCount();
}

//拓展2:當對象為nonpointer類型時纸泡,返回SideTable存儲真實的引用計數值
size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;漂问,
    //返回的是保存的真實值,沒有+1操作
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

//拓展3:當對象不是nonpointer類型時女揭,返回(SideTable_rc + 1)
uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];

    //先將返回值初始化為1蚤假,保證最小返回1
    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
        //將SideTable存儲的真實引用計數值+1返回
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

經過源碼分析可知,retainCount的實現邏輯如下:

  • Step1:如果對象為TaggedPointer對象吧兔,則直接返回當前對象磷仰。
  • Step2:如果對象是nonpointer類型,返回的引用計數值為rc = (extra_rc + 1 + SideTable_rc)境蔼。
  • Step3:如果對象不是nonpointer類型灶平,返回的引用計數值為rc = (SideTable_rc + 1)

retainCount獲取到的引用計數比真實值多1箍土,最少為1逢享。
extra_rc表示isa中存儲的引用計數值,這是系統的標記吴藻。
SideTable_rc表示SideTable中存儲的引用計數值瞒爬,這是為了書寫方便,我自己用的標記沟堡。
(extra_rc + 1 + SideTable_rc)這樣將1放在中間的書寫順序侧但,是為了提醒上面拓展2和拓展3這兩個函數的區(qū)別。

五航罗、關于引用計數的一道面試題

  • 問:alloc創(chuàng)建的對象禀横,它的引用計數為多少?
//這個NSObject對象的引用計數是多少粥血?
NSObject *obj = [[NSObject alloc] init];
  • 答:alloc創(chuàng)建的對象引用計數為0柏锄。

這道題最簡單的解答方式酿箭,是直接打印對象的引用計數

NSObject *obj = [[NSObject alloc] init];
NSLog(@" ==== rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(obj)));

//打印結果:
2020-12-28 15:53:31.838325+0800 內存管理[73461:16342967]  ==== rc = 1

retainCount函數獲取的引用計數值為1,則真實的引用計數為0趾娃,所以alloc創(chuàng)建的對象七问,引用計數為0

5.1 結論分析

為什么引用計數為0茫舶?可以將NSObject *obj = [[NSObject alloc] init]拆解成兩步來分析:

  • 第一步:通過alloc創(chuàng)建一個NSObject對象。

    在之前的alloc核心步驟相關文章里有分析alloc創(chuàng)建對象的整個流程刹淌,最后是在initInstanceIsa函數里完成了對象的isa的初始化饶氏,實現邏輯如下:

    • 如果對象不是nonpointer類型,將對象的地址賦值給isa有勾。
    • 如果對象是nonpointer類型疹启,給isa_t結構里的nonpointermagic蔼卡、has_cxx_dtor喊崖、shiftcls這4個成員進行初始化賦值。

    因此雇逞,這一步里并沒有給extra_rc賦值荤懂,后續(xù)也沒有操作extra_rc,所以alloc結束后塘砸,對象的引用計數為0节仿。

    initInstanceIsa的詳細介紹可閱讀iOS原理 alloc核心步驟3:initInstanceIsa詳解

  • 第二步:將對象的地址賦值給指針obj

    給指針賦值的操作掉蔬,不同于強引用廊宪,不會執(zhí)行retain,所以對象的引用計數依舊為0女轿。

    //給指針obj1賦值箭启,不會retain
    NSObject *obj1 = [[NSObject alloc] init];
    //對象被指針obj2強引用,會retain蛉迹,引用計數+1
    NSObject *obj2 = obj1;
    
5.1 印證結論

對這個結論可以在源碼工程中印證傅寡,這里是在objc-781源碼中進行斷點調試:

NSObject *obj = [[NSObject alloc] init];
NSLog(@" ==== rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(obj)));

obj實例化后打斷點,并在lldb中輸出isa來驗證:

(lldb) p obj
(NSObject *) $0 = 0x000000010064b810
//讀取obj對象的內存婿禽,第一個為成員isa
(lldb) x/4gx $0
0x10064b810: 0x001d800100350141 0x0000000000000000
0x10064b820: 0x64696c53534e5b2d 0x206b636172547265
//打印isa的值
(lldb) p 0x001d800100350141
(long) $1 = 8303516111405377
//這里需要聲明成isa_t的結構才能輸出
(lldb) p (isa_t)$1
(isa_t) $2 = {
  cls = NSObject
  bits = 8303516111405377
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 537305128
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}
(lldb) 

從輸出結果來看赏僧,alloc創(chuàng)建的對象,extra_rc的值為0扭倾,所以引用計數為0淀零,完美印證。這里也可以直接將isa的值0x001d800100350141以二進制展開膛壹,可以看到extra_rc的值為0驾中。(圖中紅色框內為extra_rc唉堪,__x86_64__環(huán)境)

注意,只有在源碼工程中才能這樣驗證肩民,在自己的工程中是不能輸出isa_t的結構唠亚,而且讀取內存里的isa的值,只包含了shiftcls的信息持痰。

六灶搜、總結

感覺上面已經講的很詳細了,這里就只總結幾個要點:

  • SideTable中包含三個成員:自旋鎖(slock)割卖,引用計數表(RefcountMap)患雏,弱引用表(weak_table_t)。引用計數表是個哈希表淹仑,用來存儲對象的引用計數。弱引用表也是哈希表匀借,用來存放對象的弱引用指針。

  • SideTables是一個全局的哈希數組瞬浓,里面存儲了多張SideTable,在iOS的真機模式下猿棉,最多8張屑咳,在MacOS或者模擬器模式下,最多64張兆龙。每一個對象對應一個SideTable,每一個SideTable存儲多個對象的引用計數和弱引用指針紫皇。

  • nonpointer類型的對象,引用計數存儲在isaextra_rcSideTableRefcountMap中聪铺。當被retain或者realease時化焕,先操作extra_rc,溢出或者為0時铃剔,才操作SideTable撒桨。

  • 非nonpointer類型的對象查刻,引用計數只存儲在SideTableRefcountMap中。

  • TaggedPointer對象凤类,內存由系統管理穗泵,不用處理引用計數。

  • 當對象被弱引用時谜疤,這個弱引用指針會存儲在SideTableweak_table_t中佃延。

  • 當對象被釋放時,會先執(zhí)行C++析構函數夷磕,刪除關聯對象苇侵,清空引用計數,將弱引用指針設為nil后清空企锌,最后free釋放內存空間。

  • retainCount獲取的引用計數值要比對象的真實值多1于未,最小為1撕攒。

  • alloc的對象引用計數為0。

推薦閱讀

iOS原理 alloc核心步驟3:initInstanceIsa詳解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末烘浦,一起剝皮案震驚了整個濱河市抖坪,隨后出現的幾起案子,更是在濱河造成了極大的恐慌闷叉,老刑警劉巖擦俐,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異握侧,居然都是意外死亡蚯瞧,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門品擎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埋合,“玉大人甚颂,你說我怎么就攤上這事振诬「厦矗” “怎么了禽绪?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵循捺,是天一觀的道長从橘。 經常有香客問我恰力,道長踩萎,這世上最難降的妖魔是什么香府? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任企孩,我火速辦了婚禮勿璃,結果婚禮上补疑,老公的妹妹穿的比我還像新娘癣丧。我一直安慰自己胁编,他們只是感情好嬉橙,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布市框。 她就那樣靜靜地躺著枫振,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斧拍。 梳的紋絲不亂的頭發(fā)上肆汹,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機與錄音扫腺,去河邊找鬼笆环。 笑死咧织,一個胖子當著我的面吹牛习绢,可吹牛的內容都是我干的闪萄。 我是一名探鬼主播败去,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吓妆!你這毒婦竟也來了行拢?” 一聲冷哼從身側響起舟奠,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤沼瘫,失蹤者是張志新(化名)和其女友劉穎晕鹊,沒想到半個月后溅话,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年躁锁,在試婚紗的時候發(fā)現自己被綠了战转。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片槐秧。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡刁标,死狀恐怖膀懈,靈堂內的尸體忽然破棺而出启搂,到底是詐尸還是另有隱情狐血,我是刑警寧澤匈织,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布纳决,位于F島的核電站阔加,受9級特大地震影響满钟,放射性物質發(fā)生泄漏。R本人自食惡果不足惜夭织,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望泥兰。 院中可真熱鬧,春花似錦膀捷、人聲如沸全庸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽安券。三九已至侯勉,卻和暖如春址貌,著一層夾襖步出監(jiān)牢的瞬間练对,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留螺男,地道東北人下隧。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像达传,于是被迫代替她去往敵國和親宪赶。 傳聞我的和親對象是個殘疾皇子脯燃,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內容