iOS原理 引用计数

2020-12-28  本文已影响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外,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;   //弱引用表

    //函数
    ...  ...
};

内部有三个成员:

2.2 SideTable存在多张的原因

三、引用计数的底层处理

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的实现逻辑如下:

  • 为什么是(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的实现逻辑如下:

注意:从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的实现逻辑如下:

四、获取对象的引用计数 -- retainCount()

iOS中,获取对象的引用计数有两种方式:

[obj valueForKey:@"retainCount"];
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的实现逻辑如下:

retainCount获取到的引用计数比真实值多1,最少为1。
extra_rc表示isa中存储的引用计数值,这是系统的标记。
SideTable_rc表示SideTable中存储的引用计数值,这是为了书写方便,我自己用的标记。
(extra_rc + 1 + SideTable_rc)这样将1放在中间的书写顺序,是为了提醒上面拓展2和拓展3这两个函数的区别。

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

//这个NSObject对象的引用计数是多少?
NSObject *obj = [[NSObject alloc] init];

这道题最简单的解答方式,是直接打印对象的引用计数

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]拆解成两步来分析:

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的信息。

六、总结

感觉上面已经讲的很详细了,这里就只总结几个要点:

推荐阅读

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

上一篇下一篇

猜你喜欢

热点阅读