1. 内存布局
内存五大区
在iOS中,内存主要分为栈区、堆区、全局区、常量区、代码区五大区域。如下图所示
栈区(Stack)
定义
- 栈是
系统数据结构,其对应的进程或者线程是唯一的 - 栈是
向低地址扩展的数据结构 - 栈是一块
连续的内存区域,遵循先进后出(FILO)原则 - 栈区一般在
运行时分配
存储
栈区是由编译器自动分配并释放的,主要用来存储
局部变量函数的参数,例如函数的隐藏参数(id self,SEL _cmd)
优缺点
-
优点:因为栈是由
编译器自动分配并释放的,不会产生内存碎片,所以快速高效 -
缺点:栈的
内存大小有限制,数据不灵活iOS主线程栈大小是1MB- 其他线程是
512KB MAC只有8M
以上内存大小的说明,在Threading Programming Guide中有相关说明:
堆区(Heap)
定义
- 堆是
向高地址扩展的数据结构 - 堆是
不连续的内存区域,类似于链表结构(便于增删,不便于查询),遵循先进先出(FIFO)原则 - 堆区的分配一般是在
运行时分配
存储
堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收,主要用于存放
OC中使用alloc或者 使用new开辟空间创建对象C语言中使用malloc、calloc、realloc分配的空间,需要free释放
优缺点
- 优点:灵活方便,数据适应面广泛
- 缺点:需
手动管理,速度慢、容易产生内存碎片
当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区
全局区(静态区,即.bss & .data)
全局区是编译时分配的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放
未初始化的全局变量和静态变量,即BSS区(.bss)已初始化的全局变量和静态变量,即数据区(.data)
其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量
常量区(即.rodata)
常量区是编译时分配的内存空间,在程序结束后由系统释放,主要存放
- 已经使用了的,且没有指向的
字符串常量
字符串常量因为可能在程序中被多次使用,所以`在程序运行之前就会提前分配内存
代码区(即.text)
代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
五大区验证
栈区(Stack)
验证代码如下:
- (void)testStack {
NSLog(@"************栈区************");
// 栈区
int a = 10;
int b = 20;
NSObject *object = [NSObject new];
NSLog(@"a == \t%p",&a);
NSLog(@"b == \t%p",&b);
NSLog(@"object == \t%p",&object);
NSLog(@"%lu", sizeof(&object));
NSLog(@"%lu", sizeof(a));
}
上面代码中,a、b、object都是局部变量,这些变量都存储在栈区。运行结果:
堆区(Heap)
验证代码如下:
- (void)testHeap{
NSLog(@"************堆区************");
// 堆区
NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSObject *object3 = [NSObject new];
NSLog(@"object1 = %@",object1);
NSLog(@"object2 = %@",object2);
NSLog(@"object3 = %@",object3);
// 访问---通过对象->堆区地址->存在栈区的指针
}
上面代码创建了三个变量,这三个变量都存储在栈区,这些变量存储的指针都指向堆区的对象。运行结构见下图:
全局区/常量区
案例代码如下:
int clA;
int clB = 10;
static int bssA;
static NSString *bssStr1;
static int bssB = 10;
static NSString *bssStr2 = @"hello";
static NSString *name = @"name";
- (void)testGlobal {
NSLog(@"************栈区************");
int sa = 10;
NSLog(@"bssA == \t%p",&sa);
NSLog(@"\n\n************全局区************");
NSLog(@"clA == \t%p",&clA);
NSLog(@"bssA == \t%p",&bssA);
NSLog(@"bssStr1 == \t%p",&bssStr1);
NSLog(@"clB == \t%p",&clB);
NSLog(@"bssB == \t%p",&bssB);
NSLog(@"bssStr2 == \t%p",&bssStr2);
NSLog(@"bssStr2 == \t%p",&name);
}
在上面案例中,通过打印全局区的变量的地址与栈区变量进行对比,运行结果见下图:
函数栈
函数栈又称为栈区,在内存中从高地址往低地址分配,与堆区相对,具体图示请查看文章最开始的图示栈帧是指函数(运行中且未完成)占用的一块独立的连续内存区域- 应用中新创建的
每个线程都有专用的栈空间,栈可以在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间。每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个线程完整的栈 函数调用是发生在栈上的,每个函数的相关信息(例如局部变量、调用记录等)都存储在一个栈帧中,每执行一次函数调用,就会生成一个与其相关的栈帧,然后将其栈帧压入函数栈,而当函数执行结束,则将此函数对应的栈帧出栈并释放掉
如下图所示,是经典图 - ARM的栈帧布局方式\
ARM的栈帧布局方式
- 其中
main stack frame为调用函数的栈帧 func1 stack frame为当前函数(被调用者)的栈帧栈底在高地址,栈向下增长。FP就是栈基址,它指向函数的栈帧起始地址SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序很是规矩(也比较容易被黑客攻破么),依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。- ARM也可以
用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动,ARM的特点是,两个栈空间内的地址(SP+FP)前面,必然有两个代码地址(PC+LR)明确标示着调用函数位置内的某个地址。
堆栈溢出
一般情况下应用程序是不需要考虑堆和栈的大小的,但是事实上堆和栈都不是无上限的,过多的递归会导致栈溢出,过多的alloc变量会导致堆溢出。
所以预防堆栈溢出的方法:
(1)避免层次过深的递归调用;
(2)不要使用过多的局部变量,控制局部变量的大小;
(3)避免分配占用空间太大的对象,并及时释放;
(4)实在不行,适当的情景下调用系统API修改线程的堆栈大小;
栈帧示例
描述下面代码的栈帧变化
int Add(int x,int y) {
int z = 0;
z = x + y;
return z;
}
int main() {
int a = 10;
int b = 20;
int ret = Add(a, b);
}
程序执行时栈区中栈帧的变化如下图所示
2. TiggedPointer 小对象
什么是小对象
我们知道一个对象至少要8个字节,但是对于一些数据来说是有些浪费的,比如NSNumber、NSDate、NSString(小字符串)。所以64位环境下,引入了Tagged Pointer技术,用一个小对象来存储这些数据。以字符串为例,见下图:
通过上面的案例发现,str1和str4的区别,str1的类型是NSTaggedPointerString,而str4是__NSCFString类型。同时通过控制台输出地址发现,其地址也有很大的区别
案例分析
通过具体案例来分析,案例代码:
// 案例 01
- (void)taggedPointerTest01
{
self.queue = dispatch_queue_create("com.yj.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10000; i++) {
dispatch_async(self.queue, ^{
self.nameString = [NSString stringWithFormat:@"aaa"];
NSLog(@"%@", self.nameString);
});
}
}
// 案例 02
- (void)taggedPointerTest02
{
self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameString = [NSString stringWithFormat:@"abcdefghigklmnopqrst"];
NSLog(@"%@", self.nameString);
});
}
}
分别运行上面两个案例,会有怎么样的结果呢?
taggedPointerTest01正常运行taggedPointerTest02会报错
打开汇编调试,查看taggedPointerTest02报错信息,见下图:
坏内存访问,为啥呢?因为 set 方法实际就是retain新值,release旧值。由于nameString修饰为nonatomic所以是线程不安全的。当多条线程同时访问,造成多次release,所以会出现坏内存访问。怎么解决?可以将属性 nameString 修饰符改为atomic或在循环中加锁
为啥 taggedPointerTest01 正常呢?分别在循环中打断点,如下:
taggedPointerTest01 中 nameString 是 NSTaggedPointerStirng *
taggedPointerTest02 中 nameString 是 NSCFStirng *
正常对象都是指针指向堆内存中的地址,所以taggedPointerTest02会因为多线程访问而造成坏内存访问,而TaggedPointer存储在常量区,不会创建内存。在进行对象释放时,针对TaggedPointer类型进行了过滤处理,也就说TaggedPointer类型不会对引用计数进行处理。见下面源码:
TaggedPointer 原理分析
我们在进行类的加载_read_images方法中已经探索到了TiggedPointer方面的内容。见下图:
找到 initializeTaggedPointerObfuscator,插卡混淆器源码:
也就是说上面 taggedPointerTest 案例中 p str1 打印的是混淆后的指针
搜索objc_debug_taggedpointer_obfuscator,找到针对TaggedPointer对象的指针编码和解码算法:
通过上面的算法可以发现,编码过程为:
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
解码过程为:
value ^ objc_debug_taggedpointer_obfuscator;
找到了编解码方法,可通过解码方法,将 taggedPointerTest 中 str 解码看看:
a000000000000611、a000000000000621这种解码后的地址代表啥意思呢?继续探索。。。
TaggedPointer 指针类型分析
在 objc 源码中找到 TaggedPointer
判断一个对象是否为TaggedPointer类型,通过对象指针&上_OBJC_TAG_MASK之后并等于_OBJC_TAG_MASK自己;而这个mask是高位是1,其余位都是0的。也就是说如果一个对象的最高位是1,则视为小对象
稍作修改上面的案例代码继续分析:
通过上面的案例的输出结构,大胆猜测:高位的0xa代表NSString,0xb代表NSNumber,0xe代表NSDate。
是不是这样呢?我们来看下源码:
在小对象类型进行标记时,传入了objc_tag_index_t类型的tag,查看objc_tag_index_t的定义:
上面猜测:
NSString:0xa=10=1010去掉最高位(小对象标识为)1=010=2NSNumber:0xb=11=1011去掉最高位(小对象标识为)1=011=3NSDate:0xe=14=1110去掉最高位(小对象标识为)1=110=6
这么巧么。。。 居然就猜对了。。。
TaggedPointer值分析
稍作修改上面的案例代码继续分析:
从输出中可以看出指针的末尾位表示小对象的长度。那么字符串值存储在哪呢?仔细观察可也看出 4个输出结果除了最高后一位 长度值不一样,其它位 str4包含str3->str2->str1,正如 abcd包含abc->ab->a;这难道仅仅是巧合吗?非也非也。。。
将输出的结果转为二进制,再来看看:
在终端输入 man ascii 查看 ascii 表:
通过上面可以看出,小对象的指针包含了对象类型,对象的值,对象的长度信息。
总结:通过解读源码和案例的分析,我们发下小对象在进行释放操作时会被过滤,不会执行相关的释放流程,其是存储在常量区,并不会进行内存的申请和释放,效率高了很多!
3.引用计数
我们知道iOS中内存管理分MRC和ARC,不管是哪种,都是对引用计数的处理,这些方法涉及:alloc、dealloc、realease、retain 等。MRC环境下,需要开发者手动调用这些方法,ARC环境,系统会自动调用。那么这些方法的实现原理是怎样的呢?我们逐步分析
首先回顾一下,nonpointer isa表示它不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等,使用了结构体位域,针对不同架构(arm64、x86)提供了对应的位域设置规则。其中包括了两个重要的字段:has_sidetable_rc是否有引用计数表 和 extra_rc引用计数值。
如何去分析他们之间的关系呢,在前面的章节中,已经分析了alloc的处理流程,完成isa的创建,并初始化引用计数为1。见下图:
我们知道retain会对对象的引用计数进行操作,下面从retain方法开始分析。
retain 方法
在 objc 源码中找到 retain 实现
inline id
objc_object::retain() {
// 首先判断是不是小对象
ASSERT(!isTaggedPointer());
// 调用 rootRetain
return rootRetain(false, RRVariant::FastOrMsgSend);
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
// 1. 首先判断不是是小对象
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
// 声明 新旧 isa,需要进行新旧isa的替换
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
省略部分代码 。。。
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
// 2. 如果是纯isa,判断如果是一个类,也不需要Retain操作
if (oldisa.getDecodedClass(**false**)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
// 重点
do {
transcribeToSideTable = false;
newisa = oldisa;
// 3. 不是 nonpointer isa,直接操作 散列表表
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// 4. 如果当前isa正在释放,不需要Retain操作
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
// 5. 通过 RC_ONE 读取取 bits 中 extra_rc 并 +1,同时将extra_rc是否已存满给到 carry
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();
// 6. 存储已满,修改一些标记,设置isa的extra_rc和has_sidetable_rc
// RC_HALF表示砍半,将一半存储在extra_rc
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.
// 7. 将另一半存储到散列表SideTable中
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
return (id)this;
}
retain 流程
-
【步骤一】判断是否是
TaggedPointer- 是,什么都不处理,直接返回
- 不是,进入【第二步】
-
【第二步】判断是纯
isa,并且是一个类- 是,不需要
Retain操作,直接返回 - 不是,进入【第三步】
- 是,不需要
-
【第三步】如果是纯
isa,但不是类- 是,使用散列表,进行
Retain操作 - 不是,进入【第四步】
- 是,使用散列表,进行
-
【第四步】判断当前
isa正在释放- 是,不需要
Retain操作,直接返回 - 不是,进入【第五步】
- 是,不需要
-
【第五步】通过
RC_ONE读取bits中extra_rc并+1,同时将extra_rc是否已存满给到carry进入【第六步】 -
【第六步】判断
extra_rc是否存储已满- 存储已满,修改一些标记,设置
isa的extra_rc和has_sidetable_rc,进入【第七步】 RC_HALF表示砍半,将一半存储在extra_rc- 未存满,进入【第八步】
- 存储已满,修改一些标记,设置
-
【第七步】将另一半存储到散列表
SideTable中 -
【第八步】返回,
retain流程结束
release方法
在 objc源码中找到 rootRelease 函数实现
inline void
objc_object::release() {
// 是否是小对象
ASSERT(!isTaggedPointer());
rootRelease(true, RRVariant::FastOrMsgSend);
}
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
// 1. 判断如果是TaggedPointer,什么都不处理,直接返回
if (slowpath(isTaggedPointer())) return false;
bool sideTableLocked = false;
isa_t newisa, oldisa;
oldisa = LoadExclusive(&isa.bits);
// 省略部分代码 。。。
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
// 2. 如果是纯isa,判断如果是一个类,也不需要Release操作
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}
retry:
do {
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
// 3. 如果是纯isa,使用散列表,进行Release操作
ClearExclusive(&isa.bits);
return sidetable_release(sideTableLocked, performDealloc);
}
if (slowpath(newisa.isDeallocating())) {
// 4. 如果当前isa正在释放,不需要Release操作
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
return false;
}
// don't check newisa.fast_rr; we already called any RR overrides
// 5. 通过 RC_ONE 读取取 bits 中 extra_rc 并 -1,同时将extra_rc是否已减空给到 carry
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
// 6. 如果extra_rc减空,进入underflow代码流程
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
// 如果当前isa正在释放,调用 deallocate
if (slowpath(newisa.isDeallocating()))
goto deallocate;
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
// 7. 判断当前已使用散列表存储
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;
}
// Try to remove some retain counts from the side table.
// 从散列表中取出一半
au to borrow = sidetable_subExtraRC_nolock(RC_HALF);
//如果散列表中取空了,标记emptySideTable
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;
//进行-1操作,赋值extra_rc
//通过emptySideTable标记,修改has_sidetable_rc
newisa.extra_rc = borrow.borrowed - 1; // redo the original decrement too
newisa.has_sidetable_rc = !emptySideTable;
//存储到isa的bits中
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (!stored && oldisa.nonpointer) {
// 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.
// 存储失败的补救处理
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}
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:
// Really deallocate.
// 8. 进入deallocate代码流程
ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
// 调用对象的dealloc方法
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
release流程:
-
【第一步】判断如果是
TaggedPointer- 是,什么都不处理,直接返回
- 不是,进入【第二步】
-
【第二步】判断是纯
isa,并且是一个类- 是,不需要
Release操作,直接返回 - 不是,进入【第三步】
- 是,不需要
-
【第三步】如果是纯
isa,但不是类- 是,使用散列表,进行
Release操作 - 不是,进入【第四步】
- 是,使用散列表,进行
-
【第四步】判断当前
isa正在释放- 是,不需要
Release操作,直接返回 - 不是,进入【第五步】
- 是,不需要
-
【第五步】通过
RC_ONE读取取bits中 extra_rc并-1,同时将extra_rc是否已减空给到carry,进入【第六步】 -
【第六步】判断
extra_rc是否减空- 是,进入【第七步】
- 不是,
release结束
-
【第七步】进入
underflow代码流程,判断当前已使用散列表存储- 是,从散列表中取出一半
- 如果散列表中取空了,标记
emptySideTable - 如果从散列表中取出内容,进行
-1操作,并赋值给extra_rc - 通过
emptySideTable标记,修改has_sidetable_rc - 存储到
isa的bits中 - 如果存储失败,进行补救处理
-
【第八步】进入
deallocate代码流程- 调用对象的
dealloc方法 - 返回,
release流程结束
- 调用对象的
SideTables
SideTable的结构
散列表本质就是一张哈希表
当retain操作使用SideTable进行存储时,会进入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;
}
- 本质上操作的是
SideTable结构,在SideTables中,找到当前对象对应的一张散列表。
这说明散列表不止只有一张,它的底层使用StripedMap
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
...
}
- 根据不同系统架构,可创建
8或64张SideTable。在iOS设置上,只能创建8张
查看SideTable的结构:
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(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
- 包含一把锁、一张引用计数表和一张弱引用表
所以SideTable的核心作用,对引用计数表和弱引用表进行处理
SideTable的存取
当extra_rc存满后,只会分出一半存储到SideTable中
因为对SideTable的操作,需要经过加锁、解锁保证线程安全,相对耗时。如果extra_rc存满后全部导入SideTable中,在引用计数-1的时候,需要频繁对SideTable进行操作,效率太低
相比extra_rc的操作,它通过isa的位运算得到,可以直接进行+1、-1的操作,效率要比SideTable高很多
下面对散列表SideTable中引用计数的存取进行底层分析
sidetable_retain
对散列表中的引用计数+1
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;
}
该方法的主要作用拿到当前对象的散列表中的引用计数表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_addExtraRC_nolock
当extra_rc存储已满,将一半存储到散列表中
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;
}
}
主要是从引用计数表获取引用计数后,将另一半引用计数存入相应的位置
SIDE_TABLE_RC_SHIFT的定义:
#define SIDE_TABLE_RC_SHIFT 2
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消息,如果有就借一半