前言
在上篇文章中讲述了内存的五大区与布局方面的知识,说到内存管理的方案我们第一个想到的是ARC和MRC,但在ARC和MRC中常见是TaggedPointer,NONPOINTER_ISA以及散列表,本文将对这三个方案进行详细的分析
TaggedPointer
简介
-
在WWDC2020中对
TaggedPointer进行了讲解。TaggedPointer是字面意思就是标记的指针,我们知道正常的指针指向的是内存,但对于有些类型例如NSNumber,它不需要一般对象的cache_t和method_list等,它只是一个值完全可以存在指针中,这样也大大的优化访问速度与内存,于是就产生了TaggedPointer。至于优化如下图:
- 在官方给出的解释中:
TaggedPointer主要针对一些小对象类型,例如NSNUmber,NSDate,NSIndexPath等,- 小对象将值存在本身的指针中,没有另外调用
Malloc开辟或者free释放内存, TaggedPointer有3倍内存空间的优化- 相比于
allocate/destory,TaggedPointer的速度要快106倍
案例分析
下面通过NSString案例来分析:
-
案例如下
案例中对三个长度不同的字符串进行了直接打印,copy后打印,[NSString stringWithFormat:]后打印以及[[NSString alloc] initWithFormat:]后打印,结果如下:
在打印结果中可以观察处理出现三种string的type:__NSCFConstantString、__NSTaggedPointerString和__NSCFString。下面来着重分析下NSTaggedPointerString
相关特性
模拟器
-
NSString类型
使用
p/t查看ws的二进制地址:
在WWDC2020中,我们知道在模拟器下,高位第一位是判断是否为TaggedPointer,其他位分析如下:至于类型的判断,可以对照
objc4-818.2源码objc-internal文件中的tag枚举来定位
注意事项:
1. 对于[[NSString alloc] initWithFormat:]和[NSString stringWithFormat:]类型创建的字符串,当长度不大于9位时是TaggedPointerString类型。当长度大于9时是NSCFString类型
2. 当TaggedPointerString类型字符串长度不大于7时,可以直接在地址中读取;当长度大于7时读取内容的规则就变了
-
TaggedPointerString长度为8或者9时,保存的是6位编码后的字符,且编码的字母表为eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX -
当字符串长度
大于10时,保存的是5位编码后的字符,且编码的字母表为eilotrm.apdnsIc ufkMShjTRxgC4013 -
NSNumber,NSIndexPath和NSDate的格式也类似,但是在低4位表示的就不是长度
真机
-
真机情况下大于
ws
此时表示类型和位数的位置发生了变化 -
由于我们调试时将源码中的混淆代码抽出来,然后包装成自己的
kc_objc_decodeTaggedPointer再调用,这样比较麻烦,我们可以直接设置环境变量OBJC_DISABLE_TAG_OBFUSCATION,将值置为YES,这样就无需就直接打印出来需要的地址了
面试题
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *nameStr;
// viewDidLoad
self.queue = dispatch_queue_create("com.wushuang.cn", DISPATCH_QUEUE_CONCURRENT);
- (void)taggedPointerDemo {
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"wushuang"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)cfStringDemo {
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"wushuangWelcome~"];
NSLog(@"%@",self.nameStr);
});
}
}
将2个方法分别在touchBegin中连续点击调用,结果cfStringDemo中的案例会崩溃,主要原因是nameStr赋值时会进行旧值的release和新值的retain,由于是多线程访问,所以在某个瞬间会出现多次release,进而就产生了野指针。但是taggedPointerDemo的案例就不会崩溃,唯一的区别就是一个是__NSCFString,一个是__NSTaggedPointerString,此时就需要去objc4-818.2源码中查看retain和release原理:
-
retain:- 在源码中根据查找顺序:
retain->_objc_rootRetain->objc_object::rootRetain()->objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant) - 核心代码如下:
- 如果是
taggedPointer类型,就直接返回,没有做引用计数及其它相关处理
- 在源码中根据查找顺序:
-
release:release查找顺序如下:release->_objc_rootRelease->objc_object::rootRelease()->objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)- 核心代码如下:
- 如果是
taggedPointer类型,也是直接返回,没有做其它处理
所以taggedPointer不会引起引用计数的变化,不会产生野指针导致崩溃
总结:
1.Tagged Pointer专⻔⽤来存储⼩的对象,例如NSNumber和NSDate
2.Tagged Pointer指针的值不再是地址了,⽽是真正的值。实际上它不再是⼀个对象了,只是⼀个披着对象⽪的普通变量⽽已。所以它的内存并不存储在堆中,也不需要malloc和free
3. 在内存读取上有着3倍的效率,创建时⽐以前快106倍。
NONPOINTER_ISA
- 说到
taggedPointer往往会联想到NONPOINTER_ISA,在前面的文章 iOS底层-对象的本质中我们讲解了isa的种类和结构。NONPOINTER_ISA表示是否对isa指针开启指针优化:- 0:纯isa指针
- 1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
NONPOINTER_ISA也是对存储进行了优化,使得isa中的64位得到充分的使用,其中的shiftcls和taggedPointer中的Payload类似,用来存储有效数据
NONPOINTER_ISA中的extra_rc存储的是引用计数相关数据。
我们知道引用计数与retain、release等有关,retainCount又是怎样的,下面将对他们进行分析
Retain、Release、retainCount、散列表
objc_object::rootRetain
// tryRetain: false, variant: Fast
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) { ... } //FastOrMsgSend类型的操作
if (slowpath(!oldisa.nonpointer)) { ... } // 非nonpointer操作
do {
transcribeToSideTable = false;
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(newisa.isDeallocating())) { ... } //正在析构
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (variant != RRVariant::Full) {
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;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (variant == RRVariant::Full) { // extra_rc 存满了
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;
}
从源码中可以分析出,核心代码在do-while循环,和variant == RRVariant::Full处,先来分析do-while循环代码
-
do-while循环代码先将oldisa赋值给newisa,如果newisa是非nonpointer类型,会根据判断走sidetable_tryRetain或者sidetable_retain方法,如果extra_rc存满会走carry判断进行extra_rc存一半,循环外散列表存一半操作。先来看看sidetable_tryRetain方法: -
sidetable_tryRetain: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); // 拿到SideTable中的引用计数表`RefcountMap` auto &refcnt = it.first->second; // 引用计数表中的size_t if (it.second) { // there was no entry } else if (refcnt & SIDE_TABLE_DEALLOCATING) { // 正在析构 result = false; } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { // 没有存满 refcnt += SIDE_TABLE_RC_ONE; // 引用计数增加 } return result; }-
该方法的主要作用拿到当前对象的散列表中的引用计数表
SideTable,然后判断是否在析构,判断没有存满的话就引用计数增加 -
疑问:引用计数增加的值为什么是
(1UL<<2)- 先看提到的几个宏定义的
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) //是否有过 weak 对象 #define SIDE_TABLE_DEALLOCATING (1UL<<1) //表示该对象是否正在析构 #define SIDE_TABLE_RC_ONE (1UL<<2) //从第三个 bit 开始才是存储引用计数数值的地方 #define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1)) // 最高位从字面意思得知:
- 第一个宏:是否有过 weak 对象
- 第二个宏:表示该对象是否正在析构
- 第三个宏:第三个 bit 开始才是存储引用计数数值的地方
- 第四个宏:表示在最高位占满,用来判断是否存满
-
所以引用计数需要加上
SIDE_TABLE_RC_ONE
-
-
sidetable_retain:id objc_object::sidetable_retain(bool locked) { #if SUPPORT_NONPOINTER_ISA ASSERT(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; // 获取对象的散列表 if (!locked) table.lock(); // 如果没有加锁,则调用散列表的互斥锁进行加锁 size_t& refcntStorage = table.refcnts[this]; // 获取当前引用计数 if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { //判断是否存满 refcntStorage += SIDE_TABLE_RC_ONE; // 引用计数增加 } table.unlock(); // 解锁 return (id)this; }该流程也是获取对象的散列表,然后进行引用计数增加。
后面再执行newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry)给extra_rc进行赋值,RC_ONE是开始存储extra_rc的位置。addc函数的作用是将newisa.bits和RC_ONE相加并存入newisa.bits当存满了carry值就为1。如果存满会将引用计数在extra_rc中存储一半然后做相关的标记,然后调用sidetable_addExtraRC_nolock方法
sidetable_addExtraRC_nolock:主要是从引用计数表获取引用计数后,将另一半引用计数存入相应的位置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; // isa-side bits should not be set here 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); // delta_rc << SIDE_TABLE_RC_SHIFT 是从第二个位置开始存 if (carry) { refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); // 溢出的话就refcntStorage为可存储的最大值 return true; } else { refcntStorage = newRefcnt; return false; } }
retain流程图
-
retain后整个流程如下:
objc_object::rootRelease
- 通过上面
Retain流程的分析,release的分析要容易许多,主要源码如下:
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return false;
bool sideTableLocked = false;
isa_t newisa, oldisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) { ... }
if (slowpath(!oldisa.nonpointer)) { ... }
retry:
do {
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
if (slowpath(newisa.isDeallocating())) { ... }
// 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()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
if (slowpath(newisa.isDeallocating()))
goto deallocate;
...
return false;
underflow:
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
if (!sideTableLocked) { // 未加锁相关赋值后再进行 retry
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;
}
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) {
bool didTransitionToDeallocating = false;
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
...
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// Decrement successful after borrowing from side table.
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
deallocate:
...
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
-
主要是先判断是不是小对象,如果不是再判断是不是
nonpointer- 如果不是
nonpointer就调用sidetable_release方法进行引用计数减少操作,代码如下
objc_object::sidetable_release(bool locked, bool performDealloc) { #if SUPPORT_NONPOINTER_ISA ASSERT(!isa.nonpointer); #endif SideTable& table = SideTables()[this]; bool do_dealloc = false; if (!locked) 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) { // 如果是week对象 // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it. do_dealloc = true; refcnt |= SIDE_TABLE_DEALLOCATING; } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { // 判断是否存满 refcnt -= SIDE_TABLE_RC_ONE; // 引用计数减1 } table.unlock(); if (do_dealloc && performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc)); // 调用dealloc } return do_dealloc; }当
do_dealloc为true且performDealloc为true则发送dealloc消息. - 如果不是
-
再调用
subc进行extra_rc--,如果减多了调用sidetable_subExtraRC_nolock向散列表借一半再对extra_rc进行赋值,代码如下:uintptr_t objc_object::sidetable_subExtraRC_nolock(size_t delta_rc) { ASSERT(isa.nonpointer); SideTable& table = SideTables()[this]; // 获取散列表 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, 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); size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT); // 借一半,剩余一半再存入 ASSERT(oldRefcnt > newRefcnt); // shouldn't underflow it->second = newRefcnt; return { delta_rc, newRefcnt >> SIDE_TABLE_RC_SHIFT }; }如果散列表没有就返回进行发送
dealloc消息,如果有就借一半
release流程图
retainCount
retainCount主要调用流程如下:retainCount->_objc_rootRetainCount->rootRetainCount,核心代码如下:
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
if (bits.nonpointer) {
uintptr_t rc = bits.extra_rc;
if (bits.has_sidetable_rc) { // 如果有散列表,retainCount值为 extra_rc 和引用计数表里的值相加
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount(); // 如果是非`nonpointer`,则直接取散列表
}
- 如果是
isTaggedPointer直接返回,如果是nonpointer类型,则取extra_rc,如果在散列表中存有,则取散列表中的值和extra_rc的和 - 无论是
sidetable_getExtraRC_nolock还是sidetable_retainCount方法里面都有引用计数值 >> SIDE_TABLE_RC_SHIFT的操作,主要是因为从存的时候从第二位开始存,所以计算retainCount时需要右移SIDE_TABLE_RC_SHIFT。
散列表
散列表SideTable是一个结构体,主要由互斥锁,RefcountMap引用计数表,和weak_table_t弱引用表三个成员变量组成,还有相关的析构函数和加锁方法:
在Retain和Release中,我们已经分析了RefcountMap引用计数表的相关操作,至于weak_table_t弱引用表,将在下篇文章继续分析