从runtime源码解读oc对象的引用计数原理

ARC

现在我们使用oc编程不用进行手动内存管理得益于ARC机制。ARC帮我们免去了大部分对对象的内存管理操作,其实ARC只是帮我们在合适的地方或者时间对对象进行-retain-release,并不是不用进行内存管理。

引用计数的存储

通过我之前分析的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类型的变量中。

关于SideTable

struct SideTable {
    spinlock_t slock; //操作内部数据的锁,保证线程安全
    RefcountMap refcnts;//哈希表[伪装的对象指针 : 64位的retainCoint信息值]
    weak_table_t weak_table;//存放对象弱引用指针的结构体
}
复制代码

SideTabel其实是一个包装了3个成员变量的结构体上面已注释各成员的作用,而RefcountMap refcnts这个成员就是我们稍后重点要分析的存放对象额外retainCount的成员变量。

存放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类变量可能对应多个不同的对象指针。

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哈希表中,从而释放了内存继续记录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消息,释放内存

猜你喜欢

转载自juejin.im/post/5c85d6cef265da2dd37c533a