参考文章
iOS引用计数管理之揭秘计数存储
OC源码 —— retain和release
文章目录
参考文章
tips
一、TaggedPointer和Non-pointer
1.1.TaggedPointer
1.1.1. taggedPointer数据在内存中的地址
1.1.2. 一些别的
1.1.3. 临界值测试——taggedPointer的存储形式
1.2. 优化的isa——Non-poniter
1.3. 是否开启non-pointer时isa的赋值
二、进入正题
2.1.Tagged Pointer对象
2.2. 开启了Non-pointer的isa
2.2.1. retain
源码
不溢出
一些提到的方法
实现功能
溢出
用到的方法
实现功能
由源码可知实现原理
2.2.2. release
2.3 未开启Non-pointer isa
总结
weak置nil
注释部分
tips
- 如果对象不是Tagged Pointer且关闭了Non-pointer,那该对象的引用计数就使用SideTable来存。
- isa中存放retainConut,存放在uintptr_t extra_rc,(“uintptr_t:一个能够存储指针的无符号int。这通常意味着它与指针的大小相同”)(最后的注释部分是我搜到的 没咋看明白)。最大值为2^8-1,也就是255,也就是8位二进制码全是1时的情况,所以应该是占一字节。
- 当isa中存放满了时,就分一半到sideTable,isa中继续存,再次存满后,又分一半给sidetable。
- extra_rc存放的是引用计数的值减1
一、TaggedPointer和Non-pointer
1.1.TaggedPointer
1.Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate 2:Tagged
2.Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
3.在内存读取上有着3倍的效率,创建时比以前快106倍。
目前我所知的系统中可以启用Tagged Pointer的类对象有:NSDate、NSNumber、NSString。
1.1.1. taggedPointer数据在内存中的地址
NSLog(@"%p",@(@(1).intValue));//0x127
NSLog(@"%p",@(@(2).intValue));//0x227
由此可知int类型的tag为27,因为去掉27后0x1 = 1,0x2 = 2,正好是值。所以@(1)、@(2)也不是一个对象,只是一个普通变量。
1.1.2. 一些别的
int 17:10001,5位;
long long 37:100101,6位;
double 57:111001,6位。
…
1.1.3. 临界值测试——taggedPointer的存储形式
有以下测试可知,double数据型的taggedPointer临界值为@(pow(2, 55) - 3)
NSLog(@"%p",@(pow(2, 55) - 3));//0x7ffffffffffffc57
NSLog(@"%p",@(pow(2, 55) - 2));//0x6030002c50c0
*(不太理解为什么是-3,我感觉应该是-1,这样他表示的最大值就是符合常理的数据位全是1)
2的55次方-3,结果为55位二进制码,符号位占一位,就是56位,也就是7字节,而double数据存储在内存中就是8字节,所以我们推测,tag值应该占一字节,也就是8位。
- 这个单纯就是一个地址了,没有57这个tag了,里面并没有存值的内容,所以Tagged Pointer失效了。
1.2. 优化的isa——Non-poniter
旧版的isa
typedef struct objc_object *id
struct objc_object {
Class _Nonnull isa;
}
目前的isa
struct objc_object {
private:
isa_t isa;
...
}
isa_t isa = {
Class class = Person;
uintptr_t bits = 8303516107940673;
struct {
uintptr_t nonpointer = 1;
uintptr_t has_assoc = 0;
uintptr_t has_cxx_dtor = 0;
uintptr_t shiftcls = 536872040;
uintptr_t magic = 59;
uintptr_t weakly_referenced = 0;
uintptr_t deallocating = 0;
uintptr_t has_sidetable_rc = 0;
uintptr_t extra_rc = 0;
}
}
参数介绍:
1.nonpointer
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息
2.has_assoc
是否有设置过关联对象,如果没有,释放时会更快
3.has_cxx_dtor
是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
4.shiftcls
存储着Class、Meta-Class对象的内存地址信息
5.magic
用于在调试时分辨对象是否未完成初始化
6.weakly_referenced
是否有被弱引用指向过,如果没有,释放时会更快
7.deallocating
对象是否正在释放
8.extra_rc
里面存储的值是引用计数器减1
9.has_sidetable_rc
引用计数器是否过大无法存储在isa中
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
不使用non-pointer的isa可以简化为
isa_t isa = {
Class class = Person;
}
1.3. 是否开启non-pointer时isa的赋值
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
......
(成员赋值)
......
isa = newisa;
}
}
二、进入正题
按以下顺序,满足1则不判断2,依次类推。
1:对象是否是Tagged Pointer对象;
2:对象是否启用了Non-pointer;
3:对象未启用Non-pointer。
2.1.Tagged Pointer对象
先说结论:对于Tagged Pointer对象,并没有任何的引用计数操作,引用计数数量也只是单纯的返回自己地址罢了。
retain时。
id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
if (isTaggedPointer()) return (id)this;
...
}
release时。
bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
if (isTaggedPointer()) return false;
...
}
retainCount时。
uintptr_t objc_object::rootRetainCount() {
if (isTaggedPointer()) return (uintptr_t)this;
...
}
2.2. 开启了Non-pointer的isa
这里写的简单,上面部分写的详细一点。
如果对象开启了Non-pointer,那么引用计数是存在isa中的,引用计数超过255将附加SideTable辅助存储。
2.2.1. retain
源码
[NSObject retain];
- (id)retain {
return ((id)self)->rootRetain();
}
id objc_object::rootRetain()
{
return rootRetain(false, false);
}
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 (!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;
}
源码中carry的意思是溢出,溢出也就是isa中存不下了,isa中的参数uintptr_t has_sidetable_rc : 1。
不溢出
首先我们看不溢出的情况
id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits); //获取isa的bit信息,也就是isa中的内容
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
return (id)this;
}
一些提到的方法
1.LoadExclusive: 用于返回isa的内容
LoadExclusive(&isa.bits)
static uintptr_t LoadExclusive(uintptr_t *src)
{
return *src;
}
2.addc
addc(newisa.bits, RC_ONE, 0, &carry)
# define RC_ONE (1ULL<<56)
static uintptr_t addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
{
return __builtin_addcl(lhs, rhs, carryin, carryout);
}
这个方法没搜到,官方不对外公开,根据OC源码 —— retain和release测试
实现功能
其实就是给extra_rc+1,然后更新一下isa的内容。
溢出
id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain); //内部是调用了rootretain,知识第二个参数由true变为false。
}
}
通过rootRetain_overflow内部是调用了rootretain,知识第二个参数由true变为false。所以实际源码是
id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
sidetable_addExtraRC_nolock(RC_HALF);
return (id)this;
}
# define RC_HALF (1ULL<<7)
用到的方法
rootRetain_overflow():内部是调用了rootretain,知识第二个参数由true变为false。
id objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}
sidetable_addExtraRC_nolock: 溢出后,溢出除了存放在isa中的值变为一半,剩下值的处理。
sidetable_addExtraRC_nolock(RC_HALF);
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
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;
}
}
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
#define SIDE_TABLE_RC_ONE (1UL<<2)
#define SIDE_TABLE_RC_SHIFT 2
实现功能
更新了newisa的extra_rc和has_sidetable_rc字段,将extra_rc设置为了RC_HALF也就是128,has_sidetable_rc设为true,将一半的引用计数存放到SideTable中。当isa再次溢出时,addc之后又加了128进去。说白了每次extra_rc溢出了,SideTable中就增加128。
由源码可知实现原理
isa的extra_rc用来存放引用计数值,has_sidetable_rc用来表示有无用sideTable存引用计数。
当isa中的extra_rc值小于等于255时,存放在extra_rc中,此时has_sidetable_rc值为0,而超过这个值后,超过的那一刻has_sidetable_rc的值变为1,把isa的extra_rc中的值分一半给sidetable,然后继续往isa中存,再次存满时,就再往sidetable中分一半。也就是说每次isa的extra_rc存满,就变成128,剩下的加到sidetable中。
2.2.2. release
在深入理解了retain之后,再看release的源码就很简单了。代码太长我就不贴了,总结一下release的流程:
- 最普通的情况,直接将extra_rc减1
- 如果extra_rc为0,判断has_sidetable_rc
- has_sidetable_rc = false,说明对象已经没有引用计数了,直接dealloc释放内存
- has_sidetable_rc = true,说明extra_rc有过溢出
- 从SideTable中借位成功,每次取RC_HALF,也就是128,减1之后赋给extra_rc,回到步骤1
- 从SideTable中借位失败,直接dealloc
2.3 未开启Non-pointer isa
存到sideTable中,这部分源码详见iOS引用计数管理之揭秘计数存储
总结
这片博客讲的是引用计数的存放位置,做以下总结:
针对数据分为三种:TaggedPointer对象,未开启non-pointer的isa对象,开启non-pointer的isa对象。
1.TaggedPointe对象,retainCount返回自己本身,也就是说,对于Tagged Pointer对象,并没有任何的引用计数操作,引用计数数量也只是单纯的返回自己地址罢了。
2.开启了non-pointer的isa:先存放在isa的extra_rc中,存满之后,isa的 has_sidetable_rc值由0变为1,分一半到sideTable中,然后继续往isa的extra_rc中,存满之后继续分一半。如果sideTable中也存满了,sideTable的RefCountMap的pinned由0变为1,表示不能再增加了。
3.未开启non-pointer的isa,存放在sideTable中。
weak置nil
runtime维护着一个weak表即hash表,用于存储指向对象的weak指针
Weak表是Hash表,Key是所指对象的地址,Value是Weak指针地址的数组
以对象的地址作为key,去找weak指针
触发调用arr_clear_deallocating 函数 ,根据对象的地址将所有weak指针地址的数组,遍历数组把其中的数据置为nil。
注释部分
什么是uintptr_t数据类型?
uintptr_t extra_rc = 0;(8位,所以最多存到255,到256时要存到别的地方)
第一件事,在问题提出的时候,uintptr_t不在C++中。在C99中<stdint.h>,作为可选类型。许多C++03编译器
确实提供了该文件。它也在C++11中,在,这里它还是可选的,它引用C99作为定义。
在C99中,它被定义为“一个无符号整数类型,它的属性是,任何指向void的有效指针都可以转换为此类型,然后再转换回指针为void,结果将与原始指针相比较”。
把这个当成它说的意思。它没有提到任何关于尺寸的东西。
uintptr_t可能与void它可能更大。尽管这样的C++实现方法是不正常的,但它可能会更小。例如,在某个假设的平台上void是32位,但是只使用了24位虚拟地址空间,您可以拥有24位。uintptr_t满足了要求。我不知道为什么一个实现会这样做,但标准允许这样做。
它是一个能够存储指针的无符号int。这通常意味着它与指针的大小相同。它是在C++11和更高版本的标准中定义的。
想要一个能够保存架构指针类型的整数类型的一个常见原因是对指针执行特定于整数的操作,或者通过提供一个整数“句柄”来模糊指针的类型。