iOS原理 引用计数

iOS原理 文章汇总

前言

在iOS中,对象的内存是通过引用计数(Reference Count)来管理的。每当有一个新的强引用指针指向,对象的引用计数就会+1,当减少一个强引用指针,引用计数就会-1,当引用计数为0时,对象就会被销毁。

一、引用计数值的存储

在前面介绍alloc核心步骤的相关文章中有提到,一个nonpointer类型的对象,它的引用计数是存放在成员isa_t里的extra_rc中。在__arm64__环境下,extra_rc在内存中占19位,在__x86_64__环境下,占8位。

isa_t结构里和内存管理相关的成员除extra_rc外,还有weakly_referencedhas_sidetable_rc以及deallocating这三个,具体情况可参考iOS原理 alloc核心步骤3:initInstanceIsa详解

__x86_64__环境为例,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类型对象,直接在里面存放引用计数。当对象的引用计数变为0时,会自动将相关的信息从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;   // 可能会发生的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.如果当前对象正在释放,执行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:如果对象是否正在释放,则执行dealloc流程。
  • Step4:如果对象是nonpointer类型,执行extra_rc+1,并判断extra_rc是否溢出。若已溢出,则将(extra_rc最大值 + 1)的一半分别存在 isaextra_rcSideTableRefcountMap中。
  • 为什么是(extra_rc最大值 + 1)的一半?

    x86_64环境下,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变为-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,会跳来这里执行
 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)) {
        //若当前对象正在释放,则不再执行释放操作,直接解锁SideTable,并返回一个过度释放的错误
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    //将isa的deallocating赋值为1,表示正在执行释放操作
    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);

    //发送一个dealloc消息,释放对象
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

经过源码分析可知,release的实现逻辑如下:

  • Step1:如果对象为TaggedPointer对象,则直接返回,不做其他操作。
  • Step2:如果对象不是nonpointer类型,若对象为元类对象,则直接返回,若不是元类对象,则直接操作SideTable,将对象的引用计数-1并保存在RefcountMap中,当引用计数变为0,则释放对象。
  • Step3:如果对象是nonpointer类型,执行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,就直接释放对象。若当前正在释放,则不再执行新的释放操作,并返回一个过度释放的错误。

注意:从SideTable中取出一半引用计数值后,SideTable中存储的值也只剩下一半,如果后续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:如果对象有析构器,则执行C++析构函数。
  • Step4:如果对象有关联对象,则删除关联对象。
  • Step5:如果对象有弱引用指针,则将弱引用表中当前对象关联的所有指针都设为nil并移除。
  • Step6:如果对象有在SideTable中存储引用计数,则将引用计数从表中移除。
  • Step7:若对象不是nonpointer类型,则直接在SideTable中清空对象的相关信息,包括引用计数和弱引用指针。
  • Step8:执行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这两个函数的区别。

五、关于引用计数的一道面试题

  • 问:alloc创建的对象,它的引用计数为多少?
//这个NSObject对象的引用计数是多少?
NSObject *obj = [[NSObject alloc] init];
  • 答:alloc创建的对象引用计数为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创建的对象,引用计数为0

5.1 结论分析

为什么引用计数为0?可以将NSObject *obj = [[NSObject alloc] init]拆解成两步来分析:

  • 第一步:通过alloc创建一个NSObject对象。

    在之前的alloc核心步骤相关文章里有分析alloc创建对象的整个流程,最后是在initInstanceIsa函数里完成了对象的isa的初始化,实现逻辑如下:

    • 如果对象不是nonpointer类型,将对象的地址赋值给isa
    • 如果对象是nonpointer类型,给isa_t结构里的nonpointermagichas_cxx_dtorshiftcls这4个成员进行初始化赋值。

    因此,这一步里并没有给extra_rc赋值,后续也没有操作extra_rc,所以alloc结束后,对象的引用计数为0。

    initInstanceIsa的详细介绍可阅读iOS原理 alloc核心步骤3:initInstanceIsa详解

  • 第二步:将对象的地址赋值给指针obj

    给指针赋值的操作,不同于强引用,不会执行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创建的对象,extra_rc的值为0,所以引用计数为0,完美印证。这里也可以直接将isa的值0x001d800100350141以二进制展开,可以看到extra_rc的值为0。(图中红色框内为extra_rc__x86_64__环境)

注意,只有在源码工程中才能这样验证,在自己的工程中是不能输出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中。

  • 当对象被释放时,会先执行C++析构函数,删除关联对象,清空引用计数,将弱引用指针设为nil后清空,最后free释放内存空间。

  • retainCount获取的引用计数值要比对象的真实值多1,最小为1。

  • alloc的对象引用计数为0。

推荐阅读

iOS原理 alloc核心步骤3:initInstanceIsa详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容