從runtime源碼解讀oc對象的引用計數原理

ARC

如今咱們使用oc編程不用進行手動內存管理得益於ARC機制。ARC幫咱們免去了大部分對對象的內存管理操做,其實ARC只是幫咱們在合適的地方或者時間對對象進行-retain-release,並非不用進行內存管理。c++

引用計數的存儲

經過我以前分析的oc對象內存結構能夠知道,其實對象的引用計數是存放在對象的isa指針中,isaOBJC2中是一個通過優化的指針不單存放着類對象的地址還存放着其餘有用的信息,其中就包括引用計數信息的存儲。 isa_t的結構位域中有兩個成員與引用計數有關分別是編程

uintptr_t has_sidetable_rc  : 1;      //isa_t指針第56位開始佔1位
uintptr_t extra_rc          : 8       //isa_t指針第57位開始佔8位

複製代碼

extra_rc存放的是多是對象部分或所有引用計數值減1。數組

has_sidetable_rc爲一個標誌位,值爲1時表明 extra_rc的8位內存已經不能存放下對象的retainCount , 須要把一部分retainCount存放地另外的地方。安全

retain源碼分析

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{   
    //isTaggedPointer直接返回指針
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        //標識是否須要去查找對應的SideTable
        transcribeToSideTable = false;
        
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        
        
        //這裏是isa沒有通過指針位域優化的狀況,直接進入全局變量中找出對應的SideTable類型值操做retainCount
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        
        //是否溢出的標識 , 若是調用addc函數後 isa的extra_rc++後溢出的話carry會變成非零值
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed  extra_rc 溢出了
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                
                //這裏是從新調用rootRetain參數handleOverflow = true
                return rootRetain_overflow(tryRetain);
            }
            
            //執行到這裏表明extra_rc已經移除了,須要把 extra_rc 減半 ,把那一半存放到對應的SideTable類型值中
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}
複製代碼

rootRetain主要是處理isaextra_rc中加法操做: 在extra_rc ++沒有溢出的狀況下不用特殊處理,若是溢出的話把extra_rc一半的值減掉,把減掉的值存到一個SideTable類型的變量中。bash

關於SideTable

struct SideTable {
    spinlock_t slock; //操做內部數據的鎖,保證線程安全
    RefcountMap refcnts;//哈希表[假裝的對象指針 : 64位的retainCoint信息值]
    weak_table_t weak_table;//存放對象弱引用指針的結構體
}
複製代碼

SideTabel實際上是一個包裝了3個成員變量的結構體上面已註釋各成員的做用,而RefcountMap refcnts這個成員就是咱們稍後重點要分析的存放對象額外retainCount的成員變量。ide

存放SideTable的StripedMap類型全局變量

獲取objc_object對應的SideTable類型變量函數

alignas(StripedMap<SideTable>) static uint8_t 
    SideTableBuf[sizeof(StripedMap<SideTable>)];

SideTable& table = SideTables()[this];

//函數SideTables() 實現
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
複製代碼

能夠看出全部對象對應的SideTable。都存儲在一個全局變量SideTableBuf中,把SideTableBuf定義成字符數組其目的是爲了方便計算StripedMap<SideTable>的內存大小,從而開闢一塊與StripedMap<SideTable>大小相同的內存。其實能夠把 SideTableBuf當作一個全局的StripedMap<SideTable>類型的變量,由於SideTables()方法已經把返回值SideTableBuf強轉成StripedMap<SideTable>類型的變量。下面分析下StripedMap這個類工具

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
    struct PaddedT {
        T value alignas(CacheLineSize);
    };
    
    public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
}
複製代碼

從上面定義能夠看出StripedMap<SideTable>類實際上是包裝了一個結構體的成員變量array的哈希表,該成員變量是一個裝着PaddedT類型的數組,PaddedT這個結構構體實際上就是咱們模板類傳進的SideTable。所以這裏能夠把array當作是一個裝着SideTable的容器,容量爲8或64(運行的平臺不一樣而不一樣)。源碼分析

當系統調用 SideTables()[對象指針]時,StripedMap<SideTable>這個哈希表就會在array中找出對應數組指針的SideTable類返回,這裏能夠看出其中的一個SideTable類變量可能對應多個不一樣的對象指針。post

extra_rc++ 溢出處理

if (slowpath(transcribeToSideTable)) {
   sidetable_addExtraRC_nolock(RC_HALF);
}
複製代碼

執行到下面的if語句裏面的 sidetable_addExtraRC_nolock(RC_HALF);表明通過do語句的執行邏輯得出extra_rc已經溢出了,接下來看下溢出處理的實現

// Move some retain counts to the side table from the isa field.
// Returns true if the object is now pinned.
bool 
objc_object::sidetable_addExtraRC_nolock(size_t  delta_rc)
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    //已經溢出了 直接返回true
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    //把 delta_rc 左已兩位後與 oldRefcnt 相加 判斷是否有溢出
    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    
    if (carry) {
        // 溢出處理
        // SIDE_TABLE_FLAG_MASK = 0b11 = SIDE_TABLE_DEALLOCATING + SIDE_TABLE_WEAKLY_REFERENCED
        // SIDE_TABLE_RC_PINNED 溢出標誌位
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else { //沒有溢出
        refcntStorage = newRefcnt;
        return false;
    }
}
複製代碼

能夠看出extra_c溢出的時候是把一半值減掉後存進對應對象指針的SideTable的成員變量RefcountMap refcnts中。在弄清楚上面代碼邏輯前,先看下幾個重要的宏定義

// 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))
複製代碼

經過宏定義及RefcountMap的實現(下面會分析)能夠發現refcntStorage實際上是一個8字節(64位)大小的內存其內存結構及對應的標識位以下圖

根據上面的代碼用this指針獲取存放在SideTable內部引用計數refcntStorage後,會分別判斷這3個標識位都爲0時才執行計數增長的操做,在調用addc是也會執行 delta_rc << SIDE_TABLE_RC_SHIFT 左移的操做來避開相應的標識位後在相應的內存位上。若是相加後溢出了,會把最高的移除標識位置爲1。

通過sidetable_addExtraRC_nolock處理後isa指針中的extrc_rc在溢出的狀況下成功吧一半的數值移存到了對應SideTablerefcntStorage哈希表中,從而釋放了isa.extra_rc的內存繼續記錄retainCount

關於DenseMap

咱們先看下存放extra_rc溢出部分的RefcountMap定義:

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

能夠看出 RefcountMap實際上是DenseMap的模板類的別名, DenseMap這是繼承自DenseMapBase的類,其內部實現能夠看出DenseMap實際上是一個典型的哈希表(相似oc的NSDictionary),經過分析能夠發現關於DenseMap的幾點

  1. 模板的KeyTDisguisedPtr<objc_object> 包裝對象指針,此類是對對象指針值(obje_object *)的封裝或說是假裝,使其不收內存泄露測試工具的影響。
  2. 模板的ValueTsize_t代替,size_t是一個64位內存的unsigned int
  3. 模板的KeyInfoTDenseMapInfo<KeyT>代替,在此處就至關於DenseMapInfo<DisguisedPtr<objc_object>,DenseMapInfo封裝了比較重要的方法哈希值的獲取用於查找對應Key的內容。

DenseMapInfo 實現細節

主要爲哈希表提供了KeyT的判等isEqual,以及KeyT類型值的hashValue的獲取下面是代碼實現

//Key判等實現,直接用 == 完成判等
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }

//根據Key獲取對應的hash值
static unsigned getHashValue(const T *PtrVal) {
    //ptr_hash調用到下面的內聯函數
    return ptr_hash((uintptr_t)PtrVal);
}

#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
複製代碼

DenseMap 根據 Key 查找 Value 實現

簡化了源碼看主要查找實現

//重寫操做符[]
ValueT &operator[](const KeyT &Key) {
    return FindAndConstruct(Key).second;
}

value_type& FindAndConstruct(const KeyT &Key) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return *TheBucket;

    return *InsertIntoBucket(Key, ValueT(), TheBucket);
}

//查找實現
template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    
     //存放全部內容的bucket數組
    const BucketT *BucketsPtr = getBuckets();
    
    //bucket個數
    const unsigned NumBuckets = getNumBuckets();
   
    //沒有內容直接返回
    if (NumBuckets == 0) {
      FoundBucket = 0;
      return false;
    }

    //根據Val的哈希值算出的bucket的索引 getHashValue調用的是KeyInfo的實現
    unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
    unsigned ProbeAmt = 1;
    while (1) {
    
      //從buckets數組拿出對應索引的值
      const BucketT *ThisBucket = BucketsPtr + BucketNo;

      if (KeyInfoT::isEqual(Val, ThisBucket->first)) { //符合 key == indexOfKey
        
          //賦值外面傳進來的參數
        FoundBucket = ThisBucket;
        return true;
      }
      BucketNo += ProbeAmt++;
      BucketNo&= (NumBuckets-1);
    }
}
複製代碼

release 源碼分析

首先咱們看下主要處理release邏輯的方法實現

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
複製代碼

方法主要分爲幾大邏輯模塊

  1. extra_rc --後位溢出的狀況處理
  2. ectra_rc --後下溢出的狀況處理
  3. 所有引用計數已經減掉的狀況處理

首先分析執行extra_rc--後正常未下溢出的狀況,此狀況主要是經過subc函數讓newisa.bitRC_ONE(1ULL<<56)相加,最後更新isa的值。

do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        
        //計算溢出的標識位
        uintptr_t carry;
        
         // extra_rc--   RC_ONE -> (1<<56)在isa中恰好是extra_rc的開始位
    
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); 
        
        if (slowpath(carry)) {
            goto underflow;//下溢出了, 直接跳轉下溢出的處理邏輯
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits))); // 把newisa.bits 賦值給isa.bits ,並退出 while 循環

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

複製代碼

加入通過subc函數的運算newisa.bits發生了下溢出的話,直接跳轉到underflow的處理邏輯中。下面分析下underflow的主要邏輯

underflow:
    
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) { //用SideTabel的refcnts

        //爲對應的SideTable加鎖後在操做器內存數據
        if (!sideTableLocked) {
            sidetable_lock();
            sideTableLocked = true;
             //修改下 sideTableLocked = true; 從新調用retry
            goto retry; 
        }

        // 把一部分的refCount出來賦值給 borrowed
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        
        if (borrowed > 0) {
        
            //把引用計數 - 1 後賦值給 extra_rc
            newisa.extra_rc = borrowed - 1; 
            //更新isa的extra_rc
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
                                                
            //下面是處理更新isa值失敗的重試操做
            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);
                    }
                }
            }

            //重試更新isa值仍是失敗的話,把borrowed再次存進對象的SideTable中。再週一遍retry的代碼邏輯(開始的do while位置)
            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }
            //執行到這裏表明成功把對應SideTable的值轉移了部分值到isa.ectra_rc中,併爲對應SideTable類型值加鎖
            sidetable_unlock();
            return false;
        }
        else {
            //來到else語句的話表明對應SideTable已經沒有存儲額外的retainCount。接下來要執行對象內存釋放的邏輯了。
        }
    }
複製代碼

經過上面下溢出處理的代碼分析能夠知道,extra_rc--後發生下溢出的話,系統會優先去查找對象對應SideTable值中存儲的哈希表refcnts變量,在經過refcnts查找到對應對象存儲的8字節內存的count去一部分出來(大小爲isa.extra_rc恰好溢出的一半大小),存放到isa.extra_rc中。若是此時refcnts取出的值也爲0了就表明對象能夠釋放掉內存了。

對象內存釋放的調用,主要是把isa.deallocating的標識位置爲1,而後執行SEL_dealloc釋放對象內存。

// 上面若是 borrowed == 0 來到這裏表明retainCount等於0 對象能夠釋放了
    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); //表明引用計數已經等於0  調用dealloc釋放內存
    }
    return true;
複製代碼

總結

對象經過retainrelease巧妙地使內部的isa.extra_rc與外部存儲在對應其自己的SideTable類中存儲的引用計數值增減有條不紊地進行着加減法。並經過判斷當兩個值都知足必定條件時就執行對象的SEL_dealloc消息,釋放內存

相關文章
相關標籤/搜索