前言
之前我们在WWDC20-runtime优化中有初步接触到了 TaggedPointer
。本文就讲结合一个面试题加深对它的理解。
一、 一个面试题
分析一下下面的代码会有什么问题
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *nameStr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self taggedPointerDemo];
}
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("Ryukie", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<100000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"Ryukie"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
for (int i = 0; i<100000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"RyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukie"];
NSLog(@"%@",self.nameStr);
});
}
}
@end
1.1 分析
乍一看本题考验的是多线程读写,很难想到和 TaggedPointer
有关。本题两处中的不同点是字符串的长度而已。而根据我们之前对 TaggedPointer
的了解,其中的 Payload
会用来存储数据。但是 Payload
的长度是有限的,过长的数据就无法完整保存进去。
所以,这里分析两处的指针类型是不同的。
1.2 断点调试
较短的字符串
这里发现指针类型为 NSTaggedPointerString
。明显是一个 TaggedPointer
。
较长的字符串
这里的指针类型为 __NSCFString
是一个普通的指针。
1.3 问题
在第二段代码运行中出现了野指针的崩溃。
1.4 思考
根据奔溃的现象,你可能会想到。 TaggedPointer
的 Retain
和 Release
难道和普通的有不同么?
二、 源码分析
在 OC 源码中找到:
- (id)retain {
return _objc_rootRetain(self);
}
进一步找到具体实现:
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
...
}
通过 Retain
的源码,我们可以看出,在判断是 TaggedPointer
之后就直接 return
了,没有进行任何操作。
那么 Release 呢?我们探索一下源码:
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;
...
}
发现这里和 Retain
是一样的处理。
三、 题目分析
初步了解了 Retain 和 Release 对与 TaggedPointer
之后,我们就很容易理解上面的面试题所发生的现象了
TaggedPointer
的较短的字符串没有进行 Retain Release 所以多线程异步读写过程中没有出现野指针错误- 非
TaggedPointer
的较长的字符串,由于进行了多线程异步读写操作,不断的 Retain Release ,所以可能出现野指针错误。
小结
TaggedPointer
是用于特定类型的小对象的,并不是那些类型就都是TaggedPointer
。前提是Payload
要能够装得下。Retain
Release
对TaggedPointer
的特殊处理,也提高了TaggedPointer
的效率,同时在多线程场景下的安全性也比较高。
到现在我们接触到了三种指针:
TaggedPointer
、Nonpointer isa
和纯isa
。这里我们继续深入对Retain
Release
源码进行探索,看看他们在内存管理上有什么区别。
四、 Retain
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
// 不处理 TaggedPointer 直接 return
if (slowpath(isTaggedPointer())) return (id)this;
...
do {
transcribeToSideTable = false;
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
// 非 nonpointer 通过 sidetable 处理
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
...
uintptr_t carry; // 标志 extra_rc 容量是否还能够承受
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// 超出 extra_rc 容量
// newisa.extra_rc++ overflowed
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// 保留一般的引用计数,并将另一半拷贝到 side table
// 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;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (variant == RRVariant::Full) {
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();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
return (id)this;
}
小结
TaggedPointer
不处理,直接 returnNonpointer isa
BITFIELD
和SideTable
结合使用- 当
BITFIELD
的extra_rc
容量足够的时候持续 ++ - 当
BITFIELD
的extra_rc
容量不够的时候extra_rc
减半,拷贝一半到SideTable
进行管理- 同时标志位
has_sidetable_rc
改变
- 当
纯isa
- 通过
SideTable
管理
- 通过
五、 Release
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
// 不处理 TaggedPointer 直接 return
if (slowpath(isTaggedPointer())) return false;
...
retry:
do {
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
// 非 nonpointer 通过 sidetable 处理
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
...
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
// extra_rc 见底 跳转执行 underflow
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (slowpath(newisa.isDeallocating()))
goto deallocate;
...
return false;
underflow:
// extra_rc 见底后,从 SideTable 拿回引用计数,或者销毁
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
// 如果有使用 SideTable
if (variant != RRVariant::Full) {
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.
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// 从 SideTable 移除引用计数
// Try to remove some retain counts from the side table.
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
if (borrow.borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
bool didTransitionToDeallocating = false;
// 修改 extra_rc 及标志位
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
...
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
deallocate:
// Really deallocate.
// 销毁
...
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
小结
TaggedPointer
不处理,直接 returnNonpointer isa
BITFIELD
和SideTable
结合使用- 当
BITFIELD
的extra_rc
容量未见底的时候持续 -- - 当
BITFIELD
的extra_rc
容量见底的时候- 检查标志位
has_sidetable_rc
- 使用了
SideTable
- 将
SideTable
中的引用计数转移回BITFIELD
的extra_rc
进行处理
- 将
- 未使用
SideTable
- 需要销毁
- 使用了
- 检查标志位
- 当
纯isa
- 通过
SideTable
管理
- 通过
总结
通过对 Retain
和 Release
源码的探索,发现使用 TaggedPointer
和 Nonpointer isa
在内存管理的效率上会更高。一旦 SideTable
介入,流程就回变的复杂还要上锁解锁,影响效率。虽然一般开发过程中很难遇到这种情况,但是理解了这些后,对一些问题一些现象就能更清楚的看到本质。