美文网首页
iOS原理 引用计数

iOS原理 引用计数

作者: 东篱采桑人 | 来源:发表于2020-12-28 18:47 被阅读0次

    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详解

    相关文章

      网友评论

          本文标题:iOS原理 引用计数

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