iOS 内存管理底层分析(一)- 内存相关

相关文献:
iOS 内存管理底层分析(一)- 内存相关
iOS 内存管理底层分析(二)- AutoreleasePool底层

本文掌握知识点:
1.内存的五大分区
2.内存管理方案:MRC、ARC、TaggedPointer、nonpointer_isa、SideTables、自动释放池
3.weak_table_t 弱引用表底层原理、__weak的底层原理、弱引用对象的引用计数问题
4.retain、release、dealloc 的源码分析

一、内存的五大分区

  • 堆区
    堆是向高地址扩展的数据结构;是不连续的内存区域,类似于链表结构(便于增删,不便于查询),遵循先进先出(FIFO)原则;通常以 alloc/new/malloc 方式创建的对象。内存地址以0x6开头。
    优点:灵活方便,数据适应面广泛。
    缺点:需手动管理,速度慢、容易产生内存碎片。

  • 栈区
    栈是系统数据结构,其对应的进程或线程是唯一的;栈是向低地址扩展数据结构,是一块连续的存储区域,遵循先进后出(FILO)的原则。堆区的分配一般是在运行时分配;存储局部变量、方法参数、对象的指针等。内存地址以0x7开头。
    优点:栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效,便于查询,不便于增删
    缺点:内存大小有限制,数据不灵活主线程栈大小是1MB,子线程栈大小是512KB。

  • 全局区(静态区)
    全局区是编译时分配的内存空间,程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放:未初始化的全局变量和静态变量,即bss区(.bss);已初始化的全局变量和静态变量,即数据区(.data)。内存地址以0x1开头。
    其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量。

  • 常量区
    常量区是编译时分配的内存空间,在程序结束后由系统释放,主要存放 已经使用了的,且没有指向的字符串常量

  • 代码区
    代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的

内存五大区的验证:

内存布局

介绍了内存的五大区,但其实除了内存区,还有内核区保留区
以4GB手机为例,如下所示,系统将其中的3GB给了五大区+保留区,剩余的1GB给内核区使用:

  • 内核区:系统用来进行内核处理操作的区域
  • 五大区:上面已说明
  • 保留区:预留给系统处理nil等

为什么五大区的最后内存地址是从0x00400000开始的?
主要原因是0x00000000表示nil,不能直接用nil表示一个段,所以单独给了一段内存用于处理nil等情况。

内存布局面试题
面试题:全局变量和局部变量在内存中是否有区别?如果有,是什么区别? 答案是有区别。

  • 全局变量保存在内存的全局存储区(即bss+data段),占用静态的存储单元;
  • 局部变量保存在中,只有在所在函数被调用时才动态的为变量分配存储单元。

二、内存管理方案

  • MRC - 手动引用计数
  • ARC - 自动引用计数
  • nonpointer_isa - 新版OC对象的isa指针优化
  • TaggedPointer - 小对象优化
  • SideTables - 散列表 (引用计数表和弱引用表)
  • autoreleasePool - 自动释放池

三、MRC 与 ARC (引用计数)

引用计数是管理对象声明周期的一种方式。当新建一个对象它的引用计数为1,当这个对象引用计数为0的时候,这个对象就会被销毁并释放其所占用的内存空间。

MRC

在MRC时代,系统是通过对象的引用计数来判断一个是否销毁,有以下规则:

  • 对象被创建时引用计数都为1;
  • 当对象被其他指针引用时,需要手动调用[objc retain],使对象的引用计数+1;
  • 当指针变量不再使用对象时,需要手动调用[objc release]来释放对象,使对象的引用计数-1;
  • 当一个对象的引用计数为0时,系统就会销毁这个对象。
ARC

ARC模式是在WWDC2011iOS5引入的自动管理机制,即自动引用计数。是编译器的一种特性。其规则与MRC一致,区别在于无需程序员手动插入内存管理相关代码。

结论:
1.在MRC模式下,必须遵守:谁创建,谁释放,谁引用,谁管理。
2.ARC模式不需要手动retain、release、autorelease,编译器会在适当的位置插入releaseautorelease

四、nonpointer_isa - isa指针优化

nonpointer_isa:非指针类型的isa,主要是在创建对象时,用来优化isa指针的64位地址,具体内容在Objective-C 对象的底层探索

我们知道在创建OC对象的时候,会初始化一个8字节的isa指针指向该OC对象的类对象。在旧版本的OC对象的isa指针主要记录着对象的引用计数,很显然仅仅是记录这就使用8字节(64位)是非常奢侈的。于是在新版本的OC对象对isa的64位进行了优化。

nonPointerIsa

五、TaggedPointer - 小对象

TaggedPointer:是一个被打上标记的指针,在栈上分配8字节指针(不再需要堆去分配),该指针指向的不再是地址,而是真实值。TaggedPointer专门用来处理小对象,例如NSNumber、NSDate、小NSString等。(它是在64位iOS系统下提出来的 iPhone5s以后)

NSString为例,运行下面代码打印:

    NSString *firstString = @"helloworld"; // __NSCFConstantString 常量区
    NSString *secondString = [NSString stringWithFormat:@"helloworld"]; // __NSCFString 堆区
    NSString *thirdString = @"hello"; // __NSCFConstantString 常量区
    NSString *fourthSting = [NSString stringWithFormat:@"hello"]; // NSTaggedPointerString 栈指针

    NSLog(@"%p %@",firstString,[firstString class]); // 0x1058d60c0 __NSCFConstantString
    NSLog(@"%p %@",secondString,[secondString class]); // 0x600000b7b960 __NSCFString
    NSLog(@"%p %@",thirdString,[thirdString class]); // 0x1058d60e0 __NSCFConstantString
    NSLog(@"%p %@",fourthSting,[fourthSting class]); // 0xd9d08f5a3bd7e123 NSTaggedPointerString

注意:此时打印NSTaggedPointerString类型指针的内容0xd9d08f5a3bd7e123其实是混淆过的。在下文探讨 小对象地址分析 时会介绍。

NSString的内存管理主要分为3种:

  • __NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作不会引起引用计数变化,存储在字符串常量区

  • __NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上

  • NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。
    对于NSString对象来说,当字符串是由数字、英文字母组合且长度 <= 9时,会自动成为NSTaggedPointerString类型,存储在常量区;当有中文 或者 其他特殊符号 或 长度 > 9时,会直接成为__NSCFString类型,存储在堆区

1.小对象地址分析

以NSString为例:

  • 对于一般的NSString对象指针,都是string值 + 指针地址,两者是分开的;
  • 对于TaggedPointer指针,是指针 + 值,都能在小对象中体现。所以TaggedPointer 既包含指针,也包含值。

上文中提到过NSLog(@"%p %@",fourthSting,[fourthSting class]);打印出来的取地址的内容是混淆过的。

这是因为objc4-838.1源码类的加载过程中的_objc_init -> map_images -> _read_images -> initializeTaggedPointerObfuscator

小对象进行了混淆处理

我们可以在源码中通过objc_debug_taggedpointer_obfuscator查找taggedPointer编码和解码

#define OBJC_TAG_INDEX_MASK 0x7UL
#define OBJC_TAG_INDEX_SHIFT 0

extern uintptr_t objc_debug_taggedpointer_obfuscator;
extern uint8_t objc_debug_tag60_permutations[8];

uintptr_t objc_obfuscatedTagToBasicTag(uintptr_t tag) {
    for (unsigned i = 0; i < 7; i++)
        if (objc_debug_tag60_permutations[i] == tag)
            return i;
    return 7;
}

uintptr_t
objc_decodeTaggedPointer(id ptr)
{
    uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;

    value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
    value |= objc_obfuscatedTagToBasicTag(basicTag) << OBJC_TAG_INDEX_SHIFT;
    return value;
}

static inline uintptr_t objc_basicTagToObfuscatedTag(uintptr_t tag) {
    return objc_debug_tag60_permutations[tag];
}

void *
objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

    uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << OBJC_TAG_INDEX_SHIFT;
    return (void *)value;
}

于是我们就可以通过调用objc_decodeTaggedPointer来还原真实指针内容:

其中最高位是标记是否是TaggedPointer,最低三位是看是什么类型,这里010的十进制是2 表示NSString,如下图:

可以通过objc4源码中的_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入其枚举,其中 2表示NSString3表示NSNumber

举例NSNumber

2.taggedpointer面试题
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"WJ"]; // 栈(长度<=9)
            NSLog(@"%@",self.nameStr);
        });
    }
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"安安安安"]; // 堆(中文字符)
            NSLog(@"%@",self.nameStr);
        });
    }
}

结果:引发崩溃,崩溃在第二个for循环里。
原因:在ARC环境下,这里使用多线程有可能引发对同一块堆内存多次调用了release

六、SideTables - 散列表

SideTables:散列表,在散列表中主要有两个表,分别是引用计数表弱引用表。(每次在访问SideTables时的加锁解锁操作,会降低效率的)

SideTable

但是散列表不是一张表,而是多张表。
打开objc4-838.1源码,找到objc_retain -> retain -> rootRetain 这里在本章节的 七、retain源码分析 部分会着重介绍,有兴趣可以滑下去看。主要内容就是引用计数的存储方案。找到 sidetable_addExtraRC_nolock

sidetable_addExtraRC_nolock

它是从多张表SideTables里获取该对象的SideTable
SideTables其实是StripedMap类型:

SideTables()
StripedMap

多张表并不是无限数量的,苹果设计出: SideTables在真机下8张表/模拟器下64张表 的方案,以达到运行效率和节省内存平衡目的

提问一:那我可以设计只有一张全局的表来存储所有对象的引用计数和弱引用信息吗?
答案:可以。但是每次读写对象的时候都得操作这张全局表,而这张全局每次读写都需要频繁地加速/解锁操作,这样会导致系统非常地慢。

提问二:那我可以设计每一个对象都有它单独的表来存储自己的引用计数和弱引用信息吗?
答案:可以。但是每创建一个对象会就会产生一张表,则会产生好多的表,引发大量消耗内存的问题。

1.RefcountMap - 引用计数表

RefcountMap是用来存储引用计数的。(RefcountMap在一般情况下是用不到的)。
这里的内容在本文章节 七、retain源码分析 里有分析,这里只给总结:

  • a.当isa指针不是nonpointer_isa类型的时候,该对象的引用计数就存储在SideTable里的RefcountMap;
  • b.当isa指针nonpointer_isa类型,并且nonpointer_isa里的extra_rc存满了,会把另一半的引用计数存储到SideTable里的RefcountMap里,而extra_rc只有原来的一半。
2.weak_table_t - 弱引用表(weak底层原理)
UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 旧的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的

接下来看看weak底层逻辑吧。

使用__weak修饰的指针指向一个对象时,会走源码中的objc_initWeak
打开objc4-838.1源码,搜索objc_initWeak函数

objc_initWeak

storeWeak函数就是处理__weak修饰的弱对象指向一个对象的处理:

// Update a weak variable.
// If HaveOld is true, the variable has an existing value 
//   that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be 
//   assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is 
//   deallocating or newObj's class does not support weak references. 
//   If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
// HaveOld: weak指针是否之前就指向了对象 即weakVC是否指向过oldVC
// HaveNew: weak指针是否将指向的新的对象 即weakVC是否将要指向newVC
// CrashIfDeallocating: 被弱引用的对象是否正在析构,如果析构则Crash
template <HaveOld haveOld, HaveNew haveNew,
          enum CrashIfDeallocating crashIfDeallocating>
// location: 弱引用指针的地址 即weakVC的地址
// newObj: 将被弱引用指针指向的对象 即newVC对象
static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj; // 获取 weakVC之前指向的对象oldVC,若没有指向过,则为nil
    SideTable *oldTable; // 获取 weakVC之前指向的对象oldVC的散列表,若没有指向过,则为nil
    SideTable *newTable; // 获取 weakVC将被弱引用的对象newVC的散列表

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) { // 如果weakVC曾经指向过oldVC,获取 oldVC对象 和 oldVC散列表
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) { // 如果weakVC将要指向newVC,获取 newVC散列表
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
     
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable); // 加锁
    
    // 排除异常情况(不用管):如果weakVC曾经指向过oldVC,并且weakVC指向的还不是oldVC
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    //  排除异常情况(不用管):newVC的类对象还没有被初始化,就去初始化
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) { // 如果weakVC曾经指向过oldVC,则把之前的弱引用注销掉
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    // weakVC去注册新的弱引用,并指向newVC
    if (haveNew) {
        newObj = (objc_object *)
            // 怎么注册?将weakVC的地址存储到SideTable里weak_table_t里去
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        // 如果不是TaggedPointer或nil,将isa里的是否被弱引用weakly_referenced 置为true
        if (!_objc_isTaggedPointerOrNil(newObj)) {
            newObj->setWeaklyReferenced_nolock(); // 将isa的是否被弱引用置为true
        }

        // Do not set *location anywhere else. That would introduce a race.
       // 将weakVC的地址保存newVC
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); // 解锁

    // This must be called without the locks held, as it can invoke
    // arbitrary code. In particular, even if _setWeaklyReferenced
    // is not implemented, resolveInstanceMethod: may be, and may
    // call back into the weak reference machinery.
    callSetWeaklyReferenced((id)newObj);

    return (id)newObj;
}

weak底层调用:objc_initWeak -> storeWeak

storeWeak的底层逻辑:

  • 1.如果weak指针已经指向了一个A对象,则会把weak指针地址SideTable散列表里的弱引用表weak_table_t中注销weak_unregister_no_lock
  • 2.如果weak指针要 改变指向/指向新的 B对象,则会把weak指针地址注册进SideTable散列表里的弱引用表weak_table_tweak_register_no_lock,并且将 B对象的类对象的isa里的weakly_referenced置为true,还会将weak指针指向B对象。

下面就来研究如何注册和注销的。

3.了解weak指针在弱引用表weak_table_t是如何注销和注册的(探索weak_unregister_no_lockweak_register_no_lock)。
3.1了解weak_table_t的数据结构
struct weak_table_t {
    // weak_entry_t是一个hash表,key:当前对象的地址,value: 存储弱引用指针的地址的数组
    weak_entry_t *weak_entries; // (因为一个对象可以被多个弱引用去指向)
    size_t    num_entries; // 个数
    uintptr_t mask; // 数组的长度-1(扩容相关)
    uintptr_t max_hash_displacement; // 解决hash冲突
};

// hash数组
struct weak_entry_t {
    // referent是一个动态的hash数组,存储弱引用指针的地址的数组
    DisguisedPtr<objc_object> referent;
    union {
        // 1.当前对象被弱引用个数>4时,就会用这个struct来存储弱引用指针的地址
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        // 2.当前对象被弱引用个数<=4时,就会用weak_referrer_t来存储弱引用指针的地址
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    // out_of_line用来判断,用哪种1/2方式来存储弱引用指针的地址
    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }
    
    // weak_entry_t的构造函数
    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

注意:SideTable在真机下有8张表,所以weak_table_t也有8张表。

weak_table_t是一个hash表,key:当前对象的地址;value:存储弱引用指针的地址的数组。为什么是数组?因为一个对象可以同时被多个弱引用去指向。

weak_table_t 数据结构类似于:

 // [key: value]
{
  A对象的地址: [weak1地址, weak2地址],
  B对象的地址: [weak3地址, weak4地址],
  ...
}
3.2 weak_register_no_lock将弱引用注册到对象的弱引用表weak_table_t
weak_register_no_lock

weak_register_no_lock的底层逻辑:

  • a.对TaggedPointer和nil不处理;
  • b.通过被引用对象的地址去取出weak_entry_t哈希数组,如果有哈希数组,则直接将弱引用指针地址存入这个哈希数组;
  • c.如果没有哈希数组,则创建一个,再将弱引用指针地址插入到哈希数组。
3.3 weak_unregister_no_lock将弱引用从对象的弱引用表weak_table_t中注册
weak_unregister_no_lock

weak_unregister_no_lock的底层逻辑:

  • a.通过被引用对象的地址去取出weak_entry_t哈希数组,如果有哈希数组,则将弱引用指针地址从哈希数组中移除。倘若哈希数组空了,则需要去清空这个对象的弱引用表weak_table_t
weak面试题:(弱引用对象的引用计数问题)

为什么weakVC打印引用计数是2呢?

UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 旧的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(newVC)));//1 断点这
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(weakVC)));//2 断点这

打开汇编调试Debug->Debug Workflow->Always Show Disassembly

当打印强引用newVC的引用计数时,可以看到汇编会调用CFGetRetainCount,而打印弱引用weakVC的引用计数时候,则会调用objc_loadWeakRetained

打开objc4-838.1源码,找到objc_loadWeakRetained

objc_loadWeakRetained

注意:在打印弱引用weakVC的引用计数时候,会对obj进行引用计数+1的操作,但是由于obj是一个局部变量,出了函数域则会引用计数-1。
每次打印引用weakVC的引用计数都是2,其实是一个假象而已。

七、retain源码分析

打开objc4-838.1源码,搜索objc_retain函数

objc_retain
retain()

rootRetain函数就是处理对象的引用计数的逻辑:

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指针,因为引用计数信息存储在isa里
    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }

    // 核心逻辑在这个do...while!!!
    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        // 1.不是nonpointer_isa的情况,sidetable存储引用计数
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // 下面代码逻辑 是nonpointer_isa的情况
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        // 2.是nonpointer_isa的情况,引用技术位extra_rc存得下,直接+1
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        // 3.是nonpointer_isa的情况,引用技术位extra_rc存不下,extra_rc保留一半的引用计数,并准备将另一半copy到sidetable
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            // 判断extra_rc是否超出
            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.
            // extra_rc存不下,保留一半的引用计数,并准备将另一半copy到sidetable
            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) {
        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;
}

总结:
retain源码调用流程:objc_retain --> objc_object::retain() --> objc_object::rootRetain

rootRetain操作逻辑:

  • 1.判断对象是否为taggedPointer类型,如果是 则 return;
  • 2.获取对象的isa指针里取出对象的引用计数信息;
  • 3.判断isa是否是nonpointer_isa(通常是nonpointer_isa)
    3.1 如果不是nonpointer_isa,将sidetable散列表里的引用计数+1并return。
    3.2 如果是nonpointer_isa,并且对象正在被释放,直接return。
    3.3 如果是nonpointer_isa,对象不是正在被释放,进入下一步4;
  • 4.先让isa里的extra_rc引用计数+1,判断是否能够存得下:
    4.1 如果extra_rc位能存得下,就存着。
    4.2 如果extra_rc位能存不下(少数情况才会出现),将has_sidetable_rc标志位为1,extra_rc保留一半的引用计数,将另一半的引用计数存储到sidetable

提问:为什么要设计使用extra_rc来存储引用计数,extra_rc存满了才将另一半的引用计数存储到sidetable呢?
因为sidetable每次读写都需要加锁解锁的操作,系统就没有那么快。这样设计的目的是提高性能

八、release源码分析

打开objc4-838.1源码,搜索objc_release函数

objc_release
release()

rootRelease函数就是处理对象的引用计数的逻辑:

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;

    bool sideTableLocked = false;
    // 获取对象的isa,因为引用计数信息存储在isa里
    isa_t newisa, oldisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return false;
        }
    }

retry:
    // 引用计数真正操作逻辑在do...while里
    do {
        newisa = oldisa;
        // 1.如果不是nonpointer_isa,操作sidetable里的引用计数-1
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        // 判断对象是否正在释放,如果是,则直接return
        if (slowpath(newisa.isDeallocating())) {
            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
        // 将nonpointer_isa里的引用计数位extra_rc进行-1操作
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow; // 如果extra_rc的值为0的情况,走underflow代码块
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    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;
    // 如果nonpointer_isa的sidetable标志位 has_sidetable_rc == 1
    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.
        // 将引用计数从sidetable转移回extra_rc
        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.
        // 把sidetable里的引用计数移除
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        // 如果sidetable上没有多余的东西,我们就把sidetable清理干净
        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.
            // sidetable保留引用计数-1。尝试将它们添加到内联计数中。
            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 && 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.
            // 减量成功后,从sidetable拿回引用计数给extra_rc。
            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.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa.isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

总结:
release源码调用流程:objc_release --> objc_object::release() --> objc_object::rootRelease

rootRelease操作逻辑:

  • 1.判断对象是否为taggedPointer类型,如果是则return;
  • 2.获取对象的isa指针里取出对象的引用计数信息;
  • 3.判断isa是否是nonpointer_isa(通常是nonpointer_isa)
    3.1 如果不是nonpointer_isa,就去操作sidetable里的引用计数-1,并return。
    3.2 如果是nonpointer_isa,并且对象正在被释放,直接return。
    3.3 如果是nonpointer_isa,对象不是正在被释放,进入下一步4;
  • 4.先让isa里的extra_rc引用计数-1,判断extra_rc等于0
    4.1 如果extra_rc!=0时,直接return。
    4.2 如果extra_rc==0时,判断has_sidetable_rc等于1
    4.2.1 如果has_sidetable_rc==0,说明该对象引用计数全部清零,需要被回收内存dealloc
    4.2.2 如果has_sidetable_rc==1(少数情况才会出现),说明该对象借助sidetable存储引用计数,将sidetable的引用计数赋值给extra_rc,将sidetable的引用计数清空,has_sidetable_rc赋值为0。

九、dealloc源码分析

打开objc4-838.1源码,搜索- (void)dealloc方法

dealloc
_objc_rootDealloc
rootDealloc

rootDealloc去判断:
1.如果isanonpointer_isa、没有弱引用、没有关联对象、没有析构函数、没有向sidetable借位,则去直接释放free
2.否则调用object_dispose

object_dispose
objc_destructInstance
clearDeallocating

object_dispose的逻辑:
1.如果有c++析构函数,去调用析构函数释放该对象的实例成员变量;
2.如果有关联对象,去移除关联对象;
3.清除弱引用表散列表里引用计数信息
4.释放对象 free

dealloc源码调用流程:dealloc --> _objc_rootDealloc --> objc_object::rootDealloc() --> 两个分支
1. -> free
2. -> object_dispose -> free

dealloc总结:

  • 1.判断对象是否为taggedPointer类型,如果是则 return;
  • 2.判断如果isa是nonpointer_isa没有弱引用没有关联对象没有析构函数没有向sidetable借位,则直接释放对象free。否则进入3;
  • 3.如果有c++析构函数,去调用析构函数释放该对象的实例成员变量;
  • 4.如果有关联对象,去移除关联对象;
  • 5.清除弱引用表散列表里引用计数信息
  • 6.释放对象free

说到这里本章节就结束啦,喜欢的朋友点亮⭐️!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容