目录:
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_t
的lock()
方法。
自旋锁跟互斥锁类似,在任何时刻,资源都只有一个拥有者。但不同的是,当资源被占用,对于互斥锁,资源申请者只能进入睡眠状态,而对于自旋锁,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。摘自百度百科自旋锁所以自旋锁是处于“忙等”的,省略了唤醒线程的步骤,效率较高。
(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)
此时performDealloc
和handleUnderflow
都是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++应该是永恒的。
网友评论