美文网首页iOS开发基础知识OC基础
iOS内存管理之引用计数源码解读

iOS内存管理之引用计数源码解读

作者: 永远保持一颗进取心 | 来源:发表于2019-01-05 21:42 被阅读0次

    目录:
    1.retainCount
    2.retain
    3.release

    我们都知道 ARC 和 MRC 背后的原理都是引用计数,本博客通过阅读 runtime 源码中和操作引用计数相关的函数,从而进一步了解 iOS/MacOS 平台下引用计数的实现机制。
    阅读的版本为 objc4-750

    引用计数数据结构概图:

    SideTables(哈希表,key对象地址,value 是 SideTable)
        |
        |---SideTable
        |       |
        |       |---slock(自旋锁)
        |       |
        |       |---refcnts(引用计数哈希表, key 是对象地址,value是引用计数)
        |       |
        |       |---weak_table(弱引用哈希表,key是对象地址,value 是 entry)
        |               |
        |               |---entry(也可以理解为是哈希表,存储一个对象的所有弱引用,key 是弱引用地址(id*), value 也是弱引用地址)
        |               |
        |               |
        |               |---entry
        |               |      .
        |               |      .
        |               |      .
        |
        |
        |---SideTable
        |       .
        |       .
        |       .
    

    1.retainCount

    //在 NSObject.mm
    - (NSUInteger)retainCount {
        return ((id)self)->rootRetainCount();
    }
    

    跳转到 rootRetainCount()

    //在 objc-object.h
    inline uintptr_t 
    objc_object::rootRetainCount()
    {
        //(1)
        if (isTaggedPointer()) return (uintptr_t)this;
        //(2)
        sidetable_lock();
        //(3)
        isa_t bits = LoadExclusive(&isa.bits);
        ClearExclusive(&isa.bits);
        //(4)
        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();
    }
    

    代码理解:
    (1)Tagged Pointer(引用自唐巧大神文章):

    为了节省内存和提高运行效率,对于64位程序,苹果提出了 Tagged Pointer 的概念。
    对于 NSNumber 和 NSDate 等小对象,它们的值可以直接存储在指针中。
    所以 这类对象只是普通的变量,只不过是苹果框架对它们进行了特殊的处理。

    所以这里判断是 Tagged Pointer 的话,直接返回

    (2)自旋锁:

    sidetable_lock(),最终调用的是自旋锁 spinlock_tlock() 方法。
    自旋锁跟互斥锁类似,在任何时刻,资源都只有一个拥有者。但不同的是,当资源被占用,对于互斥锁,资源申请者只能进入睡眠状态,而对于自旋锁,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。摘自百度百科自旋锁

    所以自旋锁是处于“忙等”的,省略了唤醒线程的步骤,效率较高。

    (3)LoadExclusive

    看一下 LoadExclusive()的实现

    static ALWAYS_INLINE
    uintptr_t 
    LoadExclusive(uintptr_t *src)
    {
      return *src;
    }
    

    只是把src指针的内容返回, 所以(3)处的代码
    isa_t bits = LoadExclusive(&isa.bits);
    似乎跟下面这种写法并无区别
    isa_t bits = isa.bits
    各位看官也可以看看ARM开发者网的解释,不知跟这个是否有关,或者是概念上有关。
    所以这里我们理解为 isa_t bits = isa.bits 即可,不影响阅读

    (4)读取引用计数:

    if (bits.nonpointer)这个判断 成员变量 isa 的类型
    nonpointer:表示 isa_t 的类型,0表示这是一个指向 cls 的指针(iPhone 64位之前的 isa 类型),1表示当前的 isa 并不是普通意义上的指针,而是 isa_t 联合类型,其中包含有 cls 的信息,在 shiftcls 字段中。
    摘自Objective-C 中的类结构;如果对 isa 的结构不熟悉,建议各位看官稍稍看下这篇文章Objective-C 中的类结构

    现在一般都是 64位 CPU,所以此处走进if代码块内,先把 isa 的引用计数加上,然后判断是否有引用计数存储在哈希表中,如果有,就一并加上。如果没有,则放开自旋锁,返回引用计数。

    这就是读取 Objective-C 对象引用计数的方法实现,我们可以总结几个点:
    1)64位系统下,引用计数是 isa指针引用计数 + 哈希表引用计数(如果存在的话)
    2)不知各位看官有没有注意到uintptr_t rc = 1 + bits.extra_rc;引用计数 +1 的操作,也就是说,当指针引用计数和哈希表引用计数为 0,的情况下,引用计数会返回 1。所以,我们可以推测出,当对象被初始化时,引用计数默认就是 1,而不需要额外的加 1 操作。

    2.retian

    - (id)retain {//NSObject.mm
        return ((id)self)->rootRetain();
    }
    
    ALWAYS_INLINE id 
    objc_object::rootRetain()//objc-object.h
    {
        return rootRetain(false, false);
    }
    

    看到最终是返回 rootRetain(false, false); 的值,rootRetain(false, false);的实现如下:

    //objc-object.h
    
    ALWAYS_INLINE id 
    objc_object::rootRetain(bool tryRetain, bool handleOverflow)
    {
        if (isTaggedPointer()) return (id)this;//在上一个方法已经解释过
    
        bool sideTableLocked = false;
        bool transcribeToSideTable = false;
    
        isa_t oldisa;
        isa_t newisa;
    
        do {
            transcribeToSideTable = false;
            oldisa = LoadExclusive(&isa.bits);//看上个方法相关解释
            newisa = oldisa;
            (1)
            //对于64位系统,不会走进去
            if (slowpath(!newisa.nonpointer)) {
                ClearExclusive(&isa.bits);
                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
            // tryRetain 为 false,所以此处也不会走进去
            if (slowpath(tryRetain && newisa.deallocating)) {
                ClearExclusive(&isa.bits);
                if (!tryRetain && sideTableLocked) sidetable_unlock();
                return nil;
            }
            //进位(就是小学所学的加法进位的概念)
            uintptr_t carry;
            //指针引用计数加 1
            newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
            
            (2)
            //如果有进位,即指针引用计数已经满了
            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();
                sideTableLocked = true;
                transcribeToSideTable = true;
                newisa.extra_rc = RC_HALF;
                newisa.has_sidetable_rc = true;
            }
            (3)
        } 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;
    }
    

    代码理解:
    (1)slowpath():

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

    __builtin_expect是生成高效汇编代码的一种手段,可以看看这篇文章
    这里我们只要关注 slowpath(!newisa.nonpointer)括号内的逻辑即可

    (2)有进位:

    此处不难理解:如果有进位,则把引用计数加到引用计数哈希表中。
    但此处有一个技巧要提一下,每次遇到进位,都会把指针引用计数的一半加到哈希引用计数当中,这样做的好处是当下一次执行 retain 的时候,只对 isa 进行操作,而不用读取哈希表,提高了执行效率。

    (3)while 判断:

    StoreExclusive 内部调用的是__sync_bool_compare_and_swap,参考这个

    这里我们知道这个 do-while 循环直走一次即可

    3. release

    通过对retain方法的探究,我们可以大概猜测出release 的执行过程与retain相反。

    - (oneway void)release {//NSObject.mm
        ((id)self)->rootRelease();
    }
    
    ALWAYS_INLINE bool 
    objc_object::rootRelease()//objc-object.h
    {
        return rootRelease(true, false);
    }
    

    关于修饰词oneway,查看stackoverflow
    可以知道 调用的是 rootRelease(true, false),方法实现如下

    //objc-object.h
    
    ALWAYS_INLINE bool 
    objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
    {
        if (isTaggedPointer()) return false;
    
        bool sideTableLocked = false;
    
        isa_t oldisa;
        isa_t newisa;
    
     retry:
        do {
            oldisa = LoadExclusive(&isa.bits);
            newisa = oldisa;
            //64位系统不会进去
            if (slowpath(!newisa.nonpointer)) {
                ClearExclusive(&isa.bits);
                if (sideTableLocked) sidetable_unlock();
                return sidetable_release(performDealloc);
            }
            // don't check newisa.fast_rr; we already called any RR overrides
            (1)
            //指针引用计数减 1
            uintptr_t carry;
            newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
            if (slowpath(carry)) {
                // don't ClearExclusive()
                goto underflow;
            }
        } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                                 oldisa.bits, newisa.bits)));
    
        if (slowpath(sideTableLocked)) sidetable_unlock();
        return false;
    (2) 下溢
     underflow:
        // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    
        // abandon newisa to undo the decrement
        newisa = oldisa;
         (3)哈希表引用计数减 1
        if (slowpath(newisa.has_sidetable_rc)) {
            if (!handleUnderflow) {
                ClearExclusive(&isa.bits);
                return rootRelease_underflow(performDealloc);
            }
    
            // Transfer retain count from side table to inline storage.
    
            (5)上锁
            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;
            }
              (6)从哈希表取出部分引用计数
            // Try to remove some retain counts from the side table.        
            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.
    
            if (borrowed > 0) {
                // Side table retain count decreased.
                // Try to add them to the inline count.
                newisa.extra_rc = borrowed - 1;  // redo the original decrement too
                bool stored = StoreReleaseExclusive(&isa.bits, 
                                                    oldisa.bits, newisa.bits);
                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);
                        }
                    }
                }
    
                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_unlock();
                return false;
            }
            else {
                // Side table is empty after all. Fall-through to the dealloc path.
            }
        }
    
        // Really deallocate.
        (7)释放对象流程
        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);
        }
        return true;
    }
    

    代码理解:
    (1)指针引用计数减 1:

    如果指针引用计数此时大于0,则正常往下执行return false;, 方法结束,返回值是true还是false 似乎没有影响,因为没有对返回值做处理。
    但是如果指针引用计数此时刚好为 0, 则进位carry不为0,跳到 underflow

    (2)下溢:

    指针引用计数不够减 1,则由哈希引用计数减 1(看(3)), 或者执行 delloc 流程(看(4))。

    (3)哈希引用计数减 1:

    由于handleUnderflow传入值为 false, 所以走进方法rootRelease_underflow里面

    //objc-object.h
    NEVER_INLINE bool 
    objc_object::rootRelease_underflow(bool performDealloc)
     { >  return rootRelease(performDealloc, true);
     }
    

    递归执行 rootRelease(bool performDealloc, bool handleUnderflow)
    此时 performDeallochandleUnderflow都是true
    然后会执行到 (5)给 table 上锁,然后跳转到 retry:,执行到 (6)

    (6)从哈希表取出部分引用计数:

    这里的逻辑是从哈希表中取出部分引用计数,减 1 后赋值给指针引用计数
    这里有个逻辑需要探讨一下,看一下取出哈希引用计数的方法:

    size_t 
    objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
    {
      assert(isa.nonpointer);
       SideTable& table = SideTables()[this];
    //这里没有找到对应的 value或者value = 0返回 0
      RefcountMap::iterator it = table.refcnts.find(this);
       if (it == table.refcnts.end()  ||  it->second == 0) {
          // Side table retain count is zero. Can't borrow.
           return 0;
       }
      //取出当前的引用计数值
       size_t oldRefcnt = it->second;
    
       // isa-side bits should not be set here
       assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
      assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
    //(1)引用计数值减去要取出的数值,size_t 是 unsigned long
       size_t newRefcnt = oldRefcnt - (delta_rc << >SIDE_TABLE_RC_SHIFT);
       assert(oldRefcnt > newRefcnt);  // shouldn't underflow
      it->second = newRefcnt;
       return delta_rc;
    }
    

    观察(1)处的代码,由于 size_t 是无符号整数,所以这里一定有 oldRefcnt > delta_rc。但是为什么呢?
    答:因为对 哈希引用计数的操作单位都是 RC_HALF。RC_HALF是一个宏,代表的是指针引用计数所能记录最大值的一半。可以查看之前描述 retain 方法的时候,使用的也是 RC_HALF。

    这里操作成功之后,将新的 isa 存储回去,完成引用计数减 1 操作。

    (7)释放对象流程:

    这里是逻辑是,如果已经正在释放,则打印重复释放日志信息,并crash;
    否则,正常走释放对象逻辑:将标识正在释放bit 置 1,并向自己发送 dealloc 方法

    总结:

    通过阅读源码,对我自己来说,可以打破自己对源码的神秘之感,畏惧之心,多了一份惊叹和赞赏。
    除此之外还有的感受是,C++应该是永恒的。

    相关文章

      网友评论

        本文标题:iOS内存管理之引用计数源码解读

        本文链接:https://www.haomeiwen.com/subject/lmndrqtx.html