前面一篇文章,我们讲到了系统为了优化数字字符串等类型的数据存储新增了一种NSTaggedPointer
类型,同时我们还发现,isa
指针在经过优化后,提供了19个bit位用来存储引用计数的个数。但是如果超出了这个限制呢?
引用计数 其实在绝大多数情况下,仅用优化的isa_t来记录对象的引用计数就足够了,但是当对象被引用次数超过 2^19 限制时,就轮到SideTable出场了。
首先,我们先看下超出限制之后,系统是如何将引用计数转移的
retian 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 inline id objc_object::retain () { ASSERT(!isTaggedPointer()); if (fastpath(!ISA()->hasCustomRR())) { return rootRetain(); } return ((id (*)(objc_object *, SEL))objc_msgSend)(this , @selector (retain )); }
接下来我们主要看下 引用计数+1的主要负责函数rootRetain
rootRetain 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 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; 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(); } if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil ; } uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0 , &carry); if (slowpath(carry)) { if (!handleOverflow) { ClearExclusive(&isa.bits); return rootRetain_overflow(tryRetain); } if (!tryRetain && !sideTableLocked) sidetable_lock(); sideTableLocked = true ; transcribeToSideTable = true ; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true ; } } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(transcribeToSideTable)) { sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id )this ; }
当tryRetain
标志位为1时,我们会先尝试调用sidetable_tryRetain
方法,我们先看下这个方法:
sidetable_tryRetain 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 bool objc_object::sidetable_tryRetain() {#if SUPPORT_NONPOINTER_ISA ASSERT(!isa.nonpointer);#endif SideTable& table = SideTables()[this ]; bool result = true ; auto it = table.refcnts.try_emplace(this , SIDE_TABLE_RC_ONE); auto &refcnt = it.first->second; if (it.second) { } else if (refcnt & SIDE_TABLE_DEALLOCATING) { result = false ; } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { refcnt += SIDE_TABLE_RC_ONE; } return result; }
当前的对象没有处于正在被销毁的状态时,我们会将sidetable中的引用计数+1。
如果tryRetain
标志位为0,那么我们直接调用sidetable_retain
方法对引用计数器进行+1操作,sidetable_retain
方法如下:
sidetable_retain 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 id objc_object::sidetable_retain() {#if SUPPORT_NONPOINTER_ISA ASSERT(!isa.nonpointer);#endif SideTable& table = SideTables()[this ]; table.lock(); size_t& refcntStorage = table.refcnts[this ]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id )this ; }
通过上面这个方法,我们对引用计数器完成了+1(实际上是+4)的操作,那么这里为什么会+4呢? 那是因为对于table.refcnts
,实际上并不完全是表示引用计数的值,refcnts的最后两位有特殊的标示意义:
1 2 #define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) #define SIDE_TABLE_DEALLOCATING (1UL<<1)
倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);
因此,我们每次执行retain方法时,虽然每次都是+4,但是对于引用计数真实的值来说就是+1,64位环境下只有62位是保存溢出的引用计数的.
紧接如果对象没有采用isa优化且对象没有正在销毁,我们通过调用addc
方法实现引用计数器+1的操作,这个方法会给我们一个标志值carry
,表示进行+1操作后,引用计数是否溢出。
如果发生溢出,但是此时我们不需要处理溢出:
那么我们会直接调用rootRetain_overflow
方法,我们先来看下这个方法:
rootRetain_overflow 1 2 3 4 5 NEVER_INLINE id objc_object::rootRetain_overflow(bool tryRetain) { return rootRetain(tryRetain, true ); }
很明显 这个方法实际是递归调用了rootRetain
方法,只是handleOverflow
参数值被置为yes
。而对于retain操作来说实际上是走出了刚才if (!handleOverflow)
判断。那么我们继续往下看。
如果发生溢出,且我么需要处理溢出时: 我们需要先设置标志位:
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
同时将newisa
的值更新到isa
中,保存成功后,while循环结束。
紧接着我们调用sidetable_addExtraRC_nolock
方法,下面我们再来看下这个方法:
1 2 3 4 if (slowpath(transcribeToSideTable)) { sidetable_addExtraRC_nolock(RC_HALF); }
RC_HALF
的定义如下
1 #define RC_HALF (1ULL<<18)
我们都知道NSTaggedPointer
预留了19个bit位用来存放引用计数,RC_HALF
的值刚好为 2^19 次方的一半。
我们下面来看下sidetable_addExtraRC_nolock
如何实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc) { ASSERT(isa.nonpointer); SideTable& table = SideTables()[this ]; size_t& refcntStorage = table.refcnts[this ]; size_t oldRefcnt = refcntStorage; ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0 ); ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0 ); if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true ; uintptr_t carry; size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0 , &carry); if (carry) { refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); return true ; } else { refcntStorage = newRefcnt; return false ; } }
向右偏移两位的原因是,RefcountMap refcnts的最后两位有特殊的标示意义:
1 2 #define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) #define SIDE_TABLE_DEALLOCATING (1UL<<1)
倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);
所以,64位环境下只有62位是保存溢出的引用计数的.
通过上面的介绍我们了解到了引用计数是如何在sidetable中存储的(retian方法)。那么引用计数-1的操作又是怎么实现的呢?
release 1 2 3 4 5 6 7 8 9 10 11 12 13 14 inline void objc_object::release() { ASSERT(!isTaggedPointer()); if (fastpath(!ISA()->hasCustomRR())) { rootRelease(); return ; } ((void (*)(objc_object *, SEL))objc_msgSend)(this , @selector (release)); }
从上面的代码我们看到,在没有自定义release方法时,系统默认是调用的rootRelease
方法,下面我们来看下这个方法。
rootRelease 1 2 3 4 5 6 ALWAYS_INLINE bool objc_object::rootRelease() { return rootRelease(true , false ); }
上面这个方法实际上调用了同名函数(两个默认参数),下面我们进一步看下带有两个参数的rootRelease
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 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; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (rawISA()->isMetaClass()) return false ; if (sideTableLocked) sidetable_unlock(); return sidetable_release(performDealloc); } uintptr_t carry; newisa.bits = subc(newisa.bits, RC_ONE, 0 , &carry); if (slowpath(carry)) { goto underflow; } } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(sideTableLocked)) sidetable_unlock(); return false ; underflow: newisa = oldisa; if (slowpath(newisa.has_sidetable_rc)) { if (!handleUnderflow) { ClearExclusive(&isa.bits); return rootRelease_underflow(performDealloc); } if (!sideTableLocked) { ClearExclusive(&isa.bits); sidetable_lock(); sideTableLocked = true ; goto retry; } size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); if (borrowed > 0 ) { newisa.extra_rc = borrowed - 1 ; bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); if (!stored) { 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) { sidetable_addExtraRC_nolock(borrowed); goto retry; } sidetable_unlock(); return false ; } else { } } if (slowpath(newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); } newisa.deallocating = true ; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); __c11_atomic_thread_fence(__ATOMIC_ACQUIRE); if (performDealloc) { ((void (*)(objc_object *, SEL))objc_msgSend)(this , @selector (dealloc)); } return true ; }
这个方法还是有点长的,我们看到主要是由两个内部方法retry
,underflow
组成,下面我们来一步步的整理下引用计数-1操作的具体步骤
retry sidetable_release 如果这个对象没有nonpointer
优化,且不是一个类对象,那么我们直接通过对sidetable进行-1操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 uintptr_t objc_object::sidetable_release(bool performDealloc) {#if SUPPORT_NONPOINTER_ISA ASSERT(!isa.nonpointer);#endif SideTable& table = SideTables()[this ]; bool do_dealloc = false ; table.lock(); auto it = table.refcnts.try_emplace(this , SIDE_TABLE_DEALLOCATING); auto &refcnt = it.first->second; if (it.second) { do_dealloc = true ; } else if (refcnt < SIDE_TABLE_DEALLOCATING) { do_dealloc = true ; refcnt |= SIDE_TABLE_DEALLOCATING; } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { refcnt -= SIDE_TABLE_RC_ONE; } table.unlock(); if (do_dealloc && performDealloc) { ((void (*)(objc_object *, SEL))objc_msgSend)(this , @selector (dealloc)); } return do_dealloc; }
相反,如果该对象做了nonpointer优化,那么我们直接对extra_rc进行-1操作,即
1 2 3 4 5 6 7 8 newisa.bits = subc(newisa.bits, RC_ONE, 0 , &carry); if (slowpath(carry)) { goto underflow; }
将extra_rc计数-1,如果发现-1操作之后,extra_rc的个数为0,那么就出现了向下溢出,我们需要将sideTable中的部分引用计数拿到extra_rc中记录。如果没有向下溢出,那么我们就直接将修改后的newisa同步到isa中即完成了release操作。
underflow 如果在将extra_rc进行-1操作时,出现了向下溢出的问题,那么我们需要将sideTable中的引用计数移动到extra_rc中存储。
下面我们来分析下具体过程
先判断has_sidetable_rc
是否有sidetable引用计数,如果有我们要确认是否需要处理向下溢出,如果不需要处理向下溢出,那么我们直接调用rootRelease_underflow
方法,
rootRelease_underflow 1 2 3 4 5 NEVER_INLINE uintptr_t objc_object::rootRelease_underflow(bool performDealloc) { return rootRelease(performDealloc, true ); }
很明显这个方法实际上与retain操作时处理溢出逻辑相同,将rootRelease
方法中的handleUnderflow
参数置为true,要处理向下溢出。
下面我们再来看下,需要处理向下溢出时,如果当前的sidetable处于未上锁的状态时,将sidetable上锁然后进行重试,如果sidetable未已经上锁了,那么我们会执行下面这句代码:
1 size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
sidetable_subExtraRC_nolock 返回要从sidetable移动到isa的extra_rc的值,默认是获取extra_rc可存储的长度一半的值。
如果此时从sidetable中拿到的值 > 0,那么我们要将这部分值放到isa的extra_rc中进行存储,如果取到的borrowed的值为0,那么说明sidetable中的引用计数为0,那么我们直接释放该对象即可。
StoreReleaseExclusive 上面说到如果从sidetable中获取到的值borrowed大于0,那么我们直接将newisa.extra_rc
设置为borrowed - 1
即可。
然后我们在调用StoreReleaseExclusive
方法将newisa
同步到isa
中。
如果这里StoreReleaseExclusive
方法保存失败了,那么我们需要重新调用LoadExclusive
重新声明两个变量newisa2
,oldisa2
。通过addc
方法将extra_rc
置为borrowed-1
1 newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1 ), 0 , &overflow);
然后再次调用StoreReleaseExclusive
方法将newisa2
的改动同步到isa
中。
如果StoreReleaseExclusive
方法依然保存失败,那么我们就把从sidetable中获取的borrowed
重新加到sideTable中。然后调用retry方法。
经过StoreReleaseExclusive
这一步,引用计数更新操作完成。但是如果此时的引用计数为0我们改如何操作呢?
如果引用计数更新成功,那么我们需要先判断,当前对象是否正在被释放,如果正在被释放 那么调用过度释放方法overrelease_error
overrelease_error 1 2 3 4 5 6 7 NEVER_INLINE uintptr_t objc_object::overrelease_error() { _objc_inform_now_and_on_crash("%s object %p overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug" , object_getClassName((id )this ), this ); objc_overrelease_during_dealloc_error(); return 0 ; }
这个方法主要是定义了crash信息,当一个用户正在被释放时,再次调用release方法时会导致crash,具体crash信息如上述代码。
如果当前对象没有被正在释放,那么我们将当前对象正在被释放标志位置为true newisa.deallocating = true;
同时将状态的更新同步到isa
中。如果同步失败,那么会重复走一次retry。
更新状态成功后,对sidetable的操作也结束了,我们就可以将sidetable解锁(sidetable_unlock),如果需要执行dealloc方法,那么我们调用dealloc方法进行对象释放通知。
总结 至此我们就看完了objc对于retain和release以及其对引用计数的操作,以及在retain操作时当extra_rc空间不足时,引用计数是如何从extra_rc转移到sidetable中和release操作时引用计数是如何从sidetable转移到extra_rc中的。希望看了这篇文章可以帮你更好的了解引用计数的实现。