OC 底层原理 23:内存管理(五大区/TaggedPointer/引用计数)

266 阅读15分钟

1. 内存布局

内存五大区

在iOS中,内存主要分为栈区、堆区、全局区、常量区、代码区五大区域。如下图所示

666.png

栈区(Stack)

定义

  • 栈是系统数据结构,其对应的进程或者线程是唯一
  • 栈是向低地址扩展的数据结构
  • 栈是一块连续的内存区域,遵循先进后出(FILO)原则
  • 栈区一般在运行时分配

存储

栈区是由编译器自动分配并释放的,主要用来存储

  • 局部变量
  • 函数的参数,例如函数的隐藏参数(id self,SEL _cmd)

优缺点

  • 优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效

  • 缺点:栈的内存大小有限制,数据不灵活

    • iOS主线程栈大小是1MB
    • 其他线程是512KB
    • MAC只有8M

以上内存大小的说明,在Threading Programming Guide中有相关说明:

Xnip2022-08-03_16-49-19.png

堆区(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));
}

上面代码中,abobject都是局部变量,这些变量都存储在栈区。运行结果:

Xnip2022-08-03_17-09-57.png

堆区(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);
    // 访问---通过对象->堆区地址->存在栈区的指针
}

上面代码创建了三个变量,这三个变量都存储在栈区,这些变量存储的指针都指向堆区的对象。运行结构见下图:

Xnip2022-08-03_17-12-57.png

全局区/常量区

案例代码如下:

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);
}

在上面案例中,通过打印全局区的变量的地址与栈区变量进行对比,运行结果见下图:

Xnip2022-08-03_17-26-21.png

函数栈

  • 函数栈又称为栈区,在内存中从高地址往低地址分配,与堆区相对,具体图示请查看文章最开始的图示
  • 栈帧是指函数(运行中且未完成)占用的一块独立的连续内存区域
  • 应用中新创建的每个线程都有专用的栈空间,栈可以在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个线程完整的栈
  • 函数调用是发生在栈上的,每个函数的相关信息(例如局部变量、调用记录等)都存储在一个栈帧中,每执行一次函数调用,就会生成一个与其相关的栈帧,然后将其栈帧压入函数栈,而当函数执行结束,则将此函数对应的栈帧出栈并释放掉

如下图所示,是经典图 - ARM的栈帧布局方式\

777.png

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);
}

程序执行时栈区中栈帧的变化如下图所示

888.png

2. TiggedPointer 小对象

什么是小对象

我们知道一个对象至少要8个字节,但是对于一些数据来说是有些浪费的,比如NSNumberNSDateNSString(小字符串)。所以64位环境下,引入了Tagged Pointer技术,用一个小对象来存储这些数据。以字符串为例,见下图:

Xnip2022-08-03_17-58-08.png

通过上面的案例发现,str1str4的区别,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报错信息,见下图:

Xnip2022-08-03_18-13-40.png

坏内存访问,为啥呢?因为 set 方法实际就是retain新值release旧值。由于nameString修饰为nonatomic所以是线程不安全的。当多条线程同时访问,造成多次release,所以会出现坏内存访问。怎么解决?可以将属性 nameString 修饰符改为atomic或在循环中加锁

为啥 taggedPointerTest01 正常呢?分别在循环中打断点,如下:

Xnip2022-08-03_18-22-25.png taggedPointerTest01nameStringNSTaggedPointerStirng *

Xnip2022-08-03_18-23-04.png taggedPointerTest02nameStringNSCFStirng *

正常对象都是指针指向堆内存中的地址,所以taggedPointerTest02会因为多线程访问而造成坏内存访问,而TaggedPointer存储在常量区,不会创建内存。在进行对象释放时,针对TaggedPointer类型进行了过滤处理,也就说TaggedPointer类型不会对引用计数进行处理。见下面源码:

Xnip2022-08-03_18-53-39.png

TaggedPointer 原理分析

我们在进行类的加载_read_images方法中已经探索到了TiggedPointer方面的内容。见下图:

Xnip2022-08-03_18-56-57.png

找到 initializeTaggedPointerObfuscator,插卡混淆器源码:

Xnip2022-08-03_19-01-05.png

也就是说上面 taggedPointerTest 案例中 p str1 打印的是混淆后的指针

搜索objc_debug_taggedpointer_obfuscator,找到针对TaggedPointer对象的指针编码和解码算法:

Xnip2022-08-03_19-12-46.png

通过上面的算法可以发现,编码过程为:

uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

解码过程为:

value ^ objc_debug_taggedpointer_obfuscator;

找到了编解码方法,可通过解码方法,将 taggedPointerTeststr 解码看看:

Xnip2022-08-03_19-47-01.png

a000000000000611a000000000000621这种解码后的地址代表啥意思呢?继续探索。。。

TaggedPointer 指针类型分析

objc 源码中找到 TaggedPointer Xnip2022-08-03_19-51-36.png

判断一个对象是否为TaggedPointer类型,通过对象指针&_OBJC_TAG_MASK之后并等于_OBJC_TAG_MASK自己;而这个mask是高位是1,其余位都是0的。也就是说如果一个对象的最高位是1,则视为小对象

稍作修改上面的案例代码继续分析: Xnip2022-08-03_20-02-04.png

通过上面的案例的输出结构,大胆猜测:高位的0xa代表NSString0xb代表NSNumber0xe代表NSDate。 是不是这样呢?我们来看下源码:

Xnip2022-08-03_20-06-00.png

在小对象类型进行标记时,传入了objc_tag_index_t类型的tag,查看objc_tag_index_t的定义: Xnip2022-08-03_20-09-54.png

上面猜测:

  • NSString0xa = 10 = 1010 去掉最高位(小对象标识为) 1 = 010 = 2
  • NSNumber0xb = 11 = 1011 去掉最高位(小对象标识为) 1 = 011 = 3
  • NSDate0xe = 14 = 1110 去掉最高位(小对象标识为) 1 = 110 = 6

这么巧么。。。 居然就猜对了。。。

TaggedPointer值分析

稍作修改上面的案例代码继续分析: Xnip2022-08-03_20-23-10.png

从输出中可以看出指针的末尾位表示小对象的长度。那么字符串值存储在哪呢?仔细观察可也看出 4个输出结果除了最高后一位 长度值不一样,其它位 str4包含str3->str2->str1,正如 abcd包含abc->ab->a;这难道仅仅是巧合吗?非也非也。。。

将输出的结果转为二进制,再来看看: Xnip2022-08-03_20-48-49.png

在终端输入 man ascii 查看 ascii 表: Xnip2022-08-03_20-50-45.png

通过上面可以看出,小对象的指针包含了对象类型,对象的值,对象的长度信息。

总结:通过解读源码和案例的分析,我们发下小对象在进行释放操作时会被过滤,不会执行相关的释放流程,其是存储在常量区,并不会进行内存的申请和释放,效率高了很多!

3.引用计数

我们知道iOS中内存管理分MRCARC,不管是哪种,都是对引用计数的处理,这些方法涉及:allocdeallocrealeaseretain 等。MRC环境下,需要开发者手动调用这些方法,ARC环境,系统会自动调用。那么这些方法的实现原理是怎样的呢?我们逐步分析

首先回顾一下,nonpointer isa表示它不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等,使用了结构体位域,针对不同架构(arm64、x86)提供了对应的位域设置规则。其中包括了两个重要的字段:has_sidetable_rc是否有引用计数表 和 extra_rc引用计数值。

如何去分析他们之间的关系呢,在前面的章节中,已经分析了alloc的处理流程,完成isa的创建,并初始化引用计数为1。见下图:

Xnip2022-08-04_17-35-27.png

我们知道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 读取 bitsextra_rc+1,同时将extra_rc是否已存满给到 carry 进入【第六步】

  • 【第六步】判断extra_rc是否存储已满

    • 存储已满,修改一些标记,设置isaextra_rchas_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
    • 存储到isabits
    • 如果存储失败,进行补救处理
  • 【第八步】进入deallocate代码流程

    • 调用对象的dealloc方法
    • 返回,release流程结束

SideTables

SideTable的结构

散列表本质就是一张哈希表

retain操作使用SideTable进行存储时,会进入sidetable_retain函数

Xnip2022-08-04_22-55-26.png

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
...
}
  • 根据不同系统架构,可创建864SideTable。在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消息,如果有就借一半