iOS开发基础知识OC基础

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

2019-01-05  本文已影响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++应该是永恒的。

上一篇下一篇

猜你喜欢

热点阅读