2019-06-28
一、概述
进入源码分析之前,先用两张图解释类、对象、对象引用、isa之间的关系。图一为程序加载阶段的内存分配实例。程序会在加载阶段将静态定义的类载入内存,Objective-C的类实际上是C语言结构体objc_class
,结构体包含了类的超类、协议、成员变量列表、方法列表等类的元信息。类载入内存后,会在内存中占用固定空间,因此在看到很多开源代码中,当某个类包含一个Class
类型的属性时,经常会将其声明为assign
类型,这不是因为Class
是基本数据类型,而是因为Class
常驻内存,对象不需要持有Class
只需将其赋值给属性。
![](https://img.haomeiwen.com/i12472488/cfa9ff057bf4097c.jpg)
图二为程序运行阶段构建一个类的实例时实质上经过的处理,如图中的注释所述。如果有了解过 runtime 的知识,应该知道对象objc_object
的定义中objc_object
只有一个成员变量isa
,但是类的实例占用空间当然不仅仅是保存isa
指针的8个字节空间(64位机),实际上还包括了保存实例变量的连续内存空间,也就是 Ivar Layout 空间。
![](https://img.haomeiwen.com/i12472488/bd57baaca15d08f8.jpg)
那么,问题来了。既然初始化主要包括以上内存空间,那么与对象关联的内存引用计数呢?引用计数保存什么地方?这里先给出答案,后续再详细解释。 Runtime 源代码 NSObject.mm 文件中关于内存管理的代码有大量涉及了SideTable
这个数据结构,SideTable
是Objective-C实现引用计数内存管理的两个关键点之一,另一个关键点是对象的isa
指针。
二、源码分析
2.1 基本数据结构
首先了解SideTable
相关的基本数据结构。
-
RefcountMap
:NSObject.mm的中声明了DenseMap
具体类的别名RefcountMap
,其中DenseMap
是 llvm 定义的用于内存管理的一种数据结构,右图中提取了DenseMap
的几个主要成员和方法,可知DenseMap
是一种可动态调整容量的,按 Key-Value 方式访问元素,使用二次探测法(基于链表)解决冲突的 Map 数据结构。RefcountMap
功能非常明确:保存引用计数; -
SideTable
:SideTable
类包含RefcountMap
类型的成员变量refcnts
,记录全局弱引用计数表的结构体的weak_table,自旋锁slock
。SideTable
功能是管理单张内存引用技术表; -
StripMap
:多个SideTable
实例由StripMap
类集中管理。StripMap
类实例在SideTableInit
方法中初始化并保存于SideTableBuf
静态变量中,使用SideTables()
静态方法获取StripMap
实例。
关键代码如下:
/* NSObject.mm */
/* 引用计数相关 */
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
bool trylock() { return slock.trylock(); }
// Address-ordered lock discipline for a pair of side tables.
template<bool HaveOld, bool HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<bool HaveOld, bool HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
/* NSObject.mm */
/* 引用计数相关 */
alignas(StripedMap<SideTable>) static uint8_t
SideTableBuf[sizeof(StripedMap<SideTable>)];
static void SideTableInit() {
new (SideTableBuf) StripedMap<SideTable>();
}
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
/* NSObject.mm */
/* 引用计数相关 */
void arr_init(void)
{
AutoreleasePoolPage::init();
SideTableInit();
}
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
主要类定义如下图所示:
![](https://img.haomeiwen.com/i12472488/4ad5d4c5386afdbf.jpg)
![](https://img.haomeiwen.com/i12472488/ca85e6a021c4e10c.jpg)
![](https://img.haomeiwen.com/i12472488/7e5e7101d20fa557.jpg)
2.2 objc_storeStrong函数逻辑分析
NSObject.mm文件中,void objc_storeStrong(id *location, id obj)
函数用于将obj
对象的引用存储于location
地址,实质上则是将obj
的地址存储到location
地址,注意调用了objc_retain(obj)
增加了obj
对象的引用计数,原来存储于location
地址的对象prev
则调用objc_release
将其释放。这里先分析objc_retain
的操作,objc_release
操作是其反过程。
objc_storeStrong函数调用了objc_retain(id obj)
,源代码如下。其中,isTaggedPointer()
方法用于判定obj
是否为tagged pointer。对于一个对象引用(指针),一般情况下该引用的值为对象的内存地址,而tagged pointer则直接在地址中写入对象的类和数据。Tagged pointer使用1 bit标记引用是否为Tagged pointer,Objective-C中为指定最低位;使用3 bit标记对象的类,剩余60bit用于存储对象的value;注意,如果obj是tagged pointer,由于其中已经保存了对象的类型和值,因此若 retain 一个 tagged pointer 则直接返回 tagged pointer 自身,不增加引用计数。objc_retain(id obj)
方法调用了retain()
方法,retain()
方法中的hasCustomRR()
方法用于判定对象是否有自定义的retain
函数指针,否则调用rootRetain()
,本文只分析调用rootRetain()
的逻辑分支。
Tagged pointer objects store the class and the object value in the object pointer; the "pointer" does not actually point to anything
#if !SUPPORT_TAGGED_POINTERS || !TARGET_OS_IPHONE
# define SUPPORT_MSB_TAGGED_POINTERS 0
#else
# define SUPPORT_MSB_TAGGED_POINTERS 1
#endif
#if SUPPORT_MSB_TAGGED_POINTERS
# define TAG_MASK (1ULL<<63)
#else
# define TAG_MASK 1
#endif
inline bool objc_object::isTaggedPointer()
{
return ((uintptr_t)this & TAG_MASK);
}
__attribute__((aligned(16)))
id objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
inline id objc_object::retain()
{
// UseGC is allowed here, but requires hasCustomRR.
assert(!UseGC || ISA()->hasCustomRR());
assert(!isTaggedPointer());
if (! ISA()->hasCustomRR()) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
2.2.1 rootRetain方法逻辑分析
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
的处理逻辑比较复杂,源代码如下。可以先不急看代码,后续会按步对其拆分及简化。
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
assert(!UseGC);
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 (!newisa.indexed) goto unindexed;
// don't check newisa.fast_rr; we already called any RR overrides
if (tryRetain && newisa.deallocating) goto tryfail;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (carry) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) 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 (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
if (transcribeToSideTable) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (!tryRetain && sideTableLocked) sidetable_unlock();
return (id)this;
tryfail:
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
unindexed:
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
2.2.2 rootRetain分析准备工作
在分析代码之前我们先搞清楚几个问题:
-
LoadExclusive(uintptr_t *src)
直接返回*src
,即src
指针指向的内容。代码中oldisa = LoadExclusive(&isa.bits)
看起来是oldisa
加载了isa.bits
,但是由于isa.bits
数据实际上就是isa
的数据,因此oldisa
实质上是加载了isa
,写成oldisa = (isa_t)LoadExclusive(&isa.bits)
可以更好理解; -
StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
实际调用了__sync_bool_compare_and_swap((void **)dst, (void *)oldvalue, (void *)value)
,实现功能为:比较oldvalue
和dst
指针指向的值,若两者相等则将value
写入dst
指针的内容且返回true
,否则不写入且返回false
; -
addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
实际调用了__builtin_addcl(lhs, rhs, carryin, carryout)
函数,实现功能为:计算lhs + rhs + carryin
,若64bit上溢出则在输出指针carryout
中写入1
,例如:uintptr_t result = __builtin_addcl((1ULL<<63), (1ULL<<62), ((1ULL<<62) + 2), &carry)
,结果为result = 2,carry = 1,结果上溢出;__builtin_addcl((1ULL<<63), (1ULL<<62), (1ULL<<61), &carry)
,输出carry为0,结果不上溢出; -
id
是objc_object*
的别名,也就是说对象引用实际上是指向一个objc_object
结构体的指针,objc_object
结构体只有一个成员——isa_t
类型的isa
,isa_t
是联合体,有两个成员Class cls
、uintptr_t bits
。bits
被指定为占8个字节空间的结构体,其定义根据架构区分(注释中MSB是Most Significant Bit指最高有效位,extra_rc
需要处理上溢出情况因此为MSB,LSB是Least Significant Bit,indexed
位用来判断isa
指针的类型因此为LSB),下面是arm64架构下isa_t
的bits
的位域分布,图示及源代码如下左高位、右低位,源代码中删除了x86_64及其他架构下的代码:-
indexed
:洋红区域右起第1位。0
表示普通的isa
指针存储类的地址,完全使用SideTable
管理引用计数;1
表示非指针类型,结合isa
中的数据以及SideTable
集中管理引用计数; -
has_assoc
:洋红区域右起第2位。0
表示不存在关联对象;1
表示存在关联对象 -
has_cxx_dtor
:洋红区域右起第3位。0
表示不存在其他析构函数;1
表示存在其他析构函数; -
shiftcls
:红色区域共33位。保存类的虚拟内存地址。 -
magic
:黄色区域共6位,用于非指针类型的isa
校验,arm64架构下这6位为固定值0x1a
; -
weakly_referenced
:青色区域右起第1位。对象是否被弱引用; -
deallocating
:青色区域右起第2位。对象是否已执行析构(对象over release相关提示); -
has_sidetable_rc
:青色区域右起第3位。表示该对象的引用计数是否过大,以至于在extra_rc
空间中上溢出,需要在SideTable
中保存 extra reference count; -
extra_rc
:绿色区域共19位。记录对象引用计数,在has_sidetable_rc
为true
时,需要联合SideTable
才能获取到对象的确切引用计数;
-
![](https://img.haomeiwen.com/i12472488/ce049b7bb8aaa846.jpg)
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// indexed must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# endif
// SUPPORT_NONPOINTER_ISA
#endif
};
注意:Union联合体的成员之间共享内存空间。以
isa_t
为例,cls
成员和bits
成员虽然不同,但是两者的值实际在任何时候都是一致的。例如,isa.class = [NSString class]
指定了cls
指向NSString
类的内存地址,此时查看isa.bits
会发现其值为NSString
类的内存地址;反之,isa.bits = 0xFF
,则isa.class
的值变为255
。
为进一步简化代码,我们考虑把加锁解锁相关的代码移除。在这之前先弄清楚为什么tryRetain
的情况下不需要给SideTable
加锁(至于加锁的原因太明显不深入讨论,SideTable
访问必然涉及多线程竞争问题加锁是必须的)。tryRetain
的情况下的逻辑分支调用了sidetable_tryRetain()
,其源代码如下。
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
#define SIDE_TABLE_RC_SHIFT 2
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)
bool objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
// NO SPINLOCK HERE
// _objc_rootTryRetain() is called exclusively by _objc_loadWeak(),
// which already acquired the lock on our behalf.
// fixme can't do this efficiently with os_lock_handoff_s
// if (table.slock == 0) {
// _objc_fatal("Do not call -_tryRetain.");
// }
bool result = true;
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
table.refcnts[this] = SIDE_TABLE_RC_ONE;
} else if (it->second & SIDE_TABLE_DEALLOCATING) {
result = false;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second += SIDE_TABLE_RC_ONE;
}
return result;
}
-
首先,最不起眼的前三行预编译代码实际有提供关键信息,预编译判断如果支持非指针类型
isa
,则禁止使用非指针类型isa
的对象执行tryRetain
,即tryRetain
只针对使用指针类型的isa
的对象(refCount信息只存储于SideTable
中的对象); -
其次,调用
SideTables()
静态方法,获取管理SideTable的StripedMap
,再以对象this
为关键字获取对象所对应的SideTable 记为table
,table.refcnts
为 SideTable 的内存计数表,以this
为关键字获取当前对象的内存计数refcntStorage
,注意指针isa
模式下SideTable
中保存数据的位域机构和isa.bits
不一样:SideTable
中从最低位起第1位SIDE_TABLE_WEAKLY_REFERENCED
标记是否若引用,第2位SIDE_TABLE_DEALLOCATING
标记对象是否已析构,从第3位开始保存引用计数refCount,因此内存计数递增使用的增量是SIDE_TABLE_RC_ONE
即0x04
; -
至于为什么不加锁,原因在注释中有提及,
return rootRetain(true)
只会被_objc_rootTryRetain()
方法调用,且_objc_rootTryRetain()
只会被_objc_loadWeak()
方法调用,而_objc_loadWeak()
方法内的逻辑已经获取了SideTable
的锁,因此不必在_objc_rootTryRetain()
中重复加锁;
2.2.3 rootRetain逻辑拆分——!isa.index
准备工作完毕,开始分析objc_object::rootRetain(bool tryRetain, bool handleOverflow)
的源代码。从源代码中删除加锁解锁逻辑、tryRetain相关逻辑,按照判断逻辑对rootRetain
方法进行拆分。剥离出!isa.indexed
的逻辑如下:
// rootRetain逻辑拆分——!isa.index
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
return sidetable_retain();
}
关键点是sidetable_retain()
方法,其源代码如下。首先注意到与sidetable_tryRetain()
中相同的预编译语句具有同样的含义,即sidetable_retain ()
只针对使用指针类型的isa
的对象。条件语句if(! (refcntStorage & SIDE_TABLE_RC_PINNED))
用于判断refcntStorage
的最高位为0
,仅最高位为0
才递增refcntStorage
。这里table.trylock()
是尝试加锁,若 table 已加锁则返回true
,此时table
不需要重复加锁。sidetable_retain_slow ()
的处理逻辑逻辑与sidetable_retain()
基本一致,区别仅在于加锁操作。
总结:对于指针类型的isa
,内存引用计数仅存储于 SideTable 中。
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
if (table.trylock()) {
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
return sidetable_retain_slow(table);
}
__attribute__((used,noinline,nothrow))
id objc_object::sidetable_retain_slow(SideTable& table)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
顺便一提:苹果系列操作系统中,自旋锁
OSSpinLock
存在性能缺陷从iOS 10.0开始已经被弃用。
2.2.4 rootRetain逻辑拆分——isa.deallocating
剥离出isanew.deallocating
逻辑分支,得到精简后的代码如下。因此,当对象调用rootRetain()
方法且isa.isdeallocating
为true
时,不对 SideTable 和 对象的isa
做任何操作,直接返回nil;
总结:对于标记为已析构的对象,调用retain不做关于对象引用计数的任何操作。
// rootRetain逻辑拆分——isa.deallocating
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
return nil;
}
2.2.5 rootRetain逻辑拆分——!carry
从2.2.1准备工作中已知addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
函数的carryout
输出参数用于标记结果是否上溢出。剥离出newisa.isa
累加结果不上溢出的代码逻辑如下。newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry)
用于递增newisa.bits
中的引用计数数据递增量为RC_ONE
(递增量取决于extra_rc
在isa.bits
中的位域),等价于newisa.bits.extra_rc += 1
。
总结:对于非指针类型的isa
,在对象引用计数不上溢出extra_rc
位域的情况下,对象的引用计数直接存储于isa
的extra_rc
位域。
// rootRetain逻辑拆分——!carry
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
} while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
return (id)this;
}
为什么需要do-while循环?
2.2.5 rootRetain逻辑拆分——carry
剥离出newisa.isa
累加结果上溢出的代码逻辑如下,保留了原关键注释。关键点在两块if
处理逻辑。首先,if (!handleOverflow)
中简单调用了rootRetain_overflow ()
方法,其内部只是简单调用了rootRetain(tryRetain, true)
没有额外处理,因此又会回到rootRetain
处理逻辑中。这两行逻辑可以直接忽略。
上溢出的处理过程如下:
- 设置对象的
isa.bits.extra_rc
为RC_HALF
,也就是保留一半的引用计数在extra_rc
位域; - 设置对象的
isa.bits.has_sidetable_rc
位为1
,表示使用 SideTable 协同保存对象引用计数; - 调用
sidetable_addExtraRC_nolock (RC_HALF)
转移另一半的引用计数到 SideTable;
// rootRetain逻辑拆分——carry
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// newisa.extra_rc++ overflowed
if (!handleOverflow)
return rootRetain_overflow(tryRetain);
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
} while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
return (id)this;
}
NEVER_INLINE id objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}
sidetable_addExtraRC_nolock (RC_HALF)
方法的源代码如下。之前讨论过的处理逻辑不再赘述,需要注意:
-
SIDE_TABLE_DEALLOCATING
或SIDE_TABLE_WEAKLY_REFERENCED
时不调用该方法; -
addc
操作将结果写入oldRefcnt
,也就是size_t& refcntStorage = table.refcnts[this]
内存地址中; -
当累加上溢出时,
refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK)
的含义是保留功能标志位域的内容,计数位域的最高位置为1
其余置为0
;
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
assert(isa.indexed);
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);
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}
}
SIDE_TABLE_RC_PINNED
是将 SideTable 中的对象的引用计数的64位数据的最高位作为对象引用计数上溢出的标记,是MSB,引用计数上溢出时,对象的retain、release操作不会改变引用计数位域的值。
2.3 objc_release函数逻辑分析
释放操作调用objc_release(id obj)
方法,源代码如下。关键逻辑定位到objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
方法。对于指针类型的isa
其
__attribute__((aligned(16)))
void objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
inline void objc_object::release()
{
// UseGC is allowed here, but requires hasCustomRR.
assert(!UseGC || ISA()->hasCustomRR());
assert(!isTaggedPointer());
if (! ISA()->hasCustomRR()) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
ALWAYS_INLINE bool objc_object::rootRelease()
{
return rootRelease(true, false);
}
2.3.1 rootRelease逻辑分析
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
方法源代码如下,代码有点长,但由于有前面打下的知识基础,因此不逐个拆分。
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
assert(!UseGC);
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (!newisa.indexed) goto unindexed;
// 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 (carry) goto underflow;
} while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits));
if (sideTableLocked) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (newisa.has_sidetable_rc) {
if (!handleUnderflow) {
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
sidetable_lock();
sideTableLocked = true;
if (!isa.indexed) {
// Lost a race vs the indexed -> not indexed transition
// before we got the side table lock. Stop now to avoid
// breaking the safety checks in the sidetable ExtraRC code.
goto unindexed;
}
}
// 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 = StoreExclusive(&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.indexed) {
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.
if (sideTableLocked) sidetable_unlock();
if (newisa.deallocating) {
return overrelease_error();
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
__sync_synchronize();
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return true;
unindexed:
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
方法的返回是引用计数是否下溢出,performDealloc
参数指定引用计数下溢出时 是否需要向对象发送SEL_dealloc
消息,handleUnderflow
参数指定是否已经处理了内存引用计数下溢出状况。
rootRelease(bool performDealloc, bool handleUnderflow)
方法的处理过程描述如下:
-
指针类型的
isa
时,直接走 SideTable 的引用计数递减操作,调用objc_object::sidetable_release(bool performDealloc)
,并返回sidetable_release
的处理结果,下方贴出源代码,处理逻辑大致为sidetable_retain()
方法的反操作,只是多了发送SEL_dealloc
消息的逻辑; -
非指针类型的
isa
时,isa.bits.extra_rc
递减RC_ONE
; -
isa.bits.extra_rc
递减不下溢出时,返回false; -
isa.bits.extra_rc
递减下溢出 且isa.bits.has_sidetable_rc
为0
(无 SideTable 协同保存引用计数)时:若isa.deallocating
为1
表示对象已释放抛出 over release 异常,否则将isa.bits.deallocating
置为1
,并通过((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc)
向对象发送析构消息; -
isa.bits.extra_rc
递减下溢出 且isa.bits.has_sidetable_rc
为1
(有 SideTable 协同保存引用计数)时:这部分逻辑跳转比较多,在接下来的章节做代码单独剥离。
uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
if (table.trylock()) {
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
2.3.2 rootRelease逻辑拆分——carry && isa.has_sidetable_rc
忽略以下代码,得到精简后的源代码:
-
unindexed
相关处理逻辑,underflow
中的if (!sideTableLocked)
判断是为了保证在获取到SideTable
锁之前,有unindexed transition正在进行,因此需要在此处加锁等待,并在获取到锁后重新判断isa.indexed
作相应处理; -
加锁解锁相关代码;
-
重新尝试相关逻辑,两个
if (!stored)
块中代码,都是为了在更新引用计数失败时,恢复isa.bits
及 SideTable 中的引用计数数据并重新尝试; -
handleUnderFlow
简单地使用true的handleUnderFlow
参数调用自身; -
隐藏isa.deallocating及发送SEL_delloc逻辑代码
// rootRelease逻辑拆分——carry && isa.has_sidetable_rc
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (carry) goto underflow;
} while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits));
return false;
underflow:
// newisa恢复到递减前的状态
newisa = oldisa;
if (newisa.has_sidetable_rc) {
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
if (borrowed > 0) {
newisa.extra_rc = borrowed - 1; // redo the original decrement too
StoreExclusive(&isa.bits, oldisa.bits, newisa.bits);
return false;
}
}
// isa.deallocating及发送SEL_delloc逻辑
...
}
精简后的源代码中,underflow
块中的核心代码剩下sidetable_subExtraRC_nolock
方法,其实就是从 SideTable 中取出delta_rc
的引用计数,返回取出的引用计数数量。现在疑问剩下,为什么是newisa.extra_rc = borrowed - 1
,这是因为前面newisa = oldisa
将newisa恢复到递减前的状态,因此要对其作重新减1处理。
size_t objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
assert(isa.indexed);
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;
}
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;
}
2.3.3 图示对象release向side table借位
用以下三张图表示 release操作时引用计数下溢出,并向 side table 借位过程。图中,每行左右两条二进制数据分别表示对象的isa.bits
、对象在side table中对应的引用计数SideTables[this]. refcnts[this]
。左侧:绿色区域表示isa.bits
中保存内存引用计数的extra_rc
位域,右侧:绿色区域表示 side table 中保存对象的内存引用计数的位域。第一行的初始状态是:对象执行release操作时其非指针类的isa.extra_rc
为0
。
图一表示isa
向 side table 借位成功,完成release操作。
![](https://img.haomeiwen.com/i12472488/ad2bb6a0267d20c5.jpg)
图二表示isa
向 side table 借位,side table 的内存计数已减少 RC_HALF
,但是向isa.extra_rc
中写入数据失败,第二次重新尝试写入isa.extra_rc
成功,完成release操作。
![](https://img.haomeiwen.com/i12472488/f91b3c79853f7ff7.jpg)
图三表示isa
向 side table 借位,side table 的内存计数已减少 RC_HALF
,但是向isa.extra_rc
中写入数据失败,第二次重新尝试写入isa.extra_rc
仍然失败,此时只能将借出的RC_HALF
引用计数返还给 side table,返还成功后side table 的内存计数增加RC_HALF
回到release前的状态。后续会goto retry
重新尝试release操作。
![](https://img.haomeiwen.com/i12472488/47331ac54ec48fae.jpg)
以上每张图都必须遵循两个准则:
-
release操作成功时:第一行的
isa
中的引用计数 与 side table 中的引用计数 之和 必须等于最后一行的isa
中的引用计数 与 side table 中的引用计数 之和减1。引用计数递减正是release的实现效果; -
release操作失败时:第一行的
isa
中的引用计数 与 side table 中的引用计数 之和 必须等于最后一行的isa
中的引用计数 与 side table 中的引用计数 之和。这是为了保证引用计数的数量不会在release操作过程丢失。
2.4 引用计数计算法则
在上面章节介绍了,使用指针类型isa
时,当SideTable
中该对象的引用计数下溢出,向对象发送析构消息。难道引用计数不是在归零时析构吗?怎么要等到下溢出呢?本章节就是为了回答这个问题。这里同样先给出答案:对象引用计数 = isa.extra_rc
+ SideTable
记录的引用计数 + 1,这就是为何下溢出才析构的原因。
在源代码中,从NSObject
的allocWithZone
方法开始定位关键代码,经过以下方法:
+ (instancetype)allocWithZone:(struct _NSZone *)zone
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
id class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil)
inline void objc_object::initIsa(Class cls)
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
最终定位到initIsa(Class cls, bool indexed, bool hasCxxDtor)
,同时在整个过程中没有出现任何retain
的代码,其源代码如下:
inline void objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!indexed) {
isa.cls = cls;
} else {
assert(!DisableIndexedIsa);
isa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.indexed is part of ISA_MAGIC_VALUE
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
}
objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
方法用于在对象初始化时,初始化对象的isa
。关注表示使用非指针类型isa
的else
逻辑分支,isa.bits = ISA_MAGIC_VALUE
将isa
的值设置为ISA_MAGIC_VALUE
,ISA_MAGIC_VALUE
的值是0x000001a000000001ULL
,其extra_rc
位域全0
,也就是说构建对象实例后isa.extra_rc
为0
,而此时对象引用计数为1,这就是本章开头答案等式的由来。
initIsa(Class cls)
方法中initIsa(cls, false, false)
表明,目前默认使用的还是指针类型的isa
,即isa
直接指向对象的类。
从NSObject
的retainCount
方法的实现也可以找到答案,先找关键代码,经过:
- (NSUInteger)retainCount
inline uintptr_t objc_object::rootRetainCount()
最终定位到inline uintptr_t objc_object::rootRetainCount()
,源代码如下。逻辑比较清晰就不再赘述。
inline uintptr_t objc_object::rootRetainCount()
{
assert(!UseGC);
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
if (bits.indexed) {
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();
}
三、总结
总结本文要点如下:
-
Objective-C支持使用对象的
isa
、全局的多张SideTable
单独或联合管理对象引用计数; -
当使用指针类型
isa
时,即isa.indexed
位为0
时:对象的isa
是指向对象的Class
的指针,此时对象引用计数统一由SideTable
管理; -
当使用非指针类型
isa
时,即isa.indexed
位为1
时:对象的isa
不是指针而是保存类的地址、内存管理相关的信息的二进制数据,其extra_rc
位域保存内存计数,当has_sidetable_rc
位为0
时,isa.extra_rc
即是对象的引用计数,当has_sidetable_rc
位为1
时,对象引用计数是,SideTable
中记录的该对象引用计数 及isa.extra_rc
之和; -
当
isa.indexed
位为1
且has_sidetable_rc
位为1
时:retain操作若isa.extra
递增后上溢出,则转移一半的内存引用计数RC_HALF
到SideTable
; -
当
isa.indexed
位为1
且has_sidetable_rc
位为1
时:release操作若isa.extra
递减后下溢出,则向SideTable
借出RC_HALF
引用计数; -
对象引用计数 =
isa.extra_rc
+SideTable
记录的引用计数 + 1,对象引用计数归零时向对象发送SEL_dealloc
析构消息。
网友评论