iOS之深入解析内存管理retain与release的底层原理

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析内存管理retain与release的底层原理相关的知识,希望对你有一定的参考价值。

一、内存管理

① 内存管理原理
  • ios 的每个对象内部都保存了一个与之相关联的整数,称为引用计数器(auto reference count);
  • 每当使用 alloc、new 或者 copy 创建一个对象时,对象的引用计数被设置为 1;
  • 给对象发送一条 retain 消息(即调用 retain 方法),可以使引用计数 +1;
  • 给对象发送一条 release 消息,可以使引用计数 -1;
  • 当一个对象的引用计数为 0 时,那么它将被销毁,其占用的内存被系统回收,OC 会自动向对象发送一条 dealloc 消息,一般会重写 dealloc 方法释放相关资源(注意:一定不要直接调用 dealloc 方法)。
  • 可以给对象发送 retainCount 消息获得当前的引用计数。
② 引用计数是如何进行操作的?
  • 我们可以向对象发送一些消息来操作对象的引用计数,发送 retain 消息可以添加一个引用计数,发送 release 消息可以减少一个引用计数;
  • 当一个对象的引用计数为零的时候它会被立即回收。
③ 内存管理原则
  • 当对象被创建出来以后,对象的引用计数默认是 1,所以在这个对象使用完毕以后,应该为这个对象发送一条 release 消息,保证这个对象在使用完毕以后引用计数变为零,并且占用的内存空间被回收;
  • 当对象被别人使用的时候,别人就会为这个对象发送 retain 消息,表示使用的人多了一个,当别人不在使用对象的时候,别人就会为对象发送 release 消息,表示使用的人少了一个;
  • 当对象还正在被使用的时候,对象就不应该被回收;
  • 谁发送了 retain 消息,当使用完毕之后,谁就要发送 release 消息;
  • 当一个对象的引用计数为 0 时,系统就会销毁这个对象。

二、retain 与 release 作用

① retain
  • 与 assign 相对,要解决对象被其它对象引用后释放造成的问题,就要用 retain 来声明。
  • retain 声明后的对象会更改引用计数,那么每次被引用,引用计数都会+1,释放后就会-1,即使这个对象本身释放了,只要还有对象在引用它,就会持有。
  • NSString 在 retain 之后,地址相同(建立一个指针,指针拷贝),内容相同,这个对象的 retain 值 +1,也就是说 retain 是指针拷贝,在拷贝之前,都会释放旧的对象。
② release
  • 给对象发送一条 release 消息,可以使引用计数 -1;
  • 只有当对象的引用计数为 0 时,就被 dealloc 析构函数回收内存。

三、retain 与 release 底层分析

① retain
  • 进入 objc_retain -> retain -> rootRetain 的源码实现,可以看到两个逻辑:判断是否为 Nonpointer_isa 和操作引用计数。
  • 如果不是 Nonpointer_isa,则直接操作 SideTables 散列表,此时的散列表并不是只有一张,而是有多张。
  • 判断对象是否正在释放,如果正在被释放,则执行 dealloc 流程。
  • 执行 extra_rc+1,即引用计数 +1 操作,并给一个引用计数的状态标识 carry,用于表示 extra_rc 是否已满。
  • 如果 carray 的状态表示 extra_rc 的引用计数已满,此时需要操作散列表,即将满状态的一半拿出来存储到 extra_rc,另一半存储到散列表的 rc_half(这样的好处是:如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时并且消耗性能大,对半分的操作目的在于提高性能)。
	ALWAYS_INLINE id 
	objc_object::rootRetain(bool tryRetain, bool handleOverflow)
	{
	    if (isTaggedPointer()) return (id)this;
	
	    bool sideTableLocked = false;
	    bool transcribeToSideTable = false;
	    // 为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
	    isa_t oldisa;
	    isa_t newisa;

	    do {
	        transcribeToSideTable = false;
	        oldisa = LoadExclusive(&isa.bits);
	        newisa = oldisa;
	        // 判断是否为nonpointer isa
	        if (slowpath(!newisa.nonpointer)) {
	            // 如果不是 nonpointer isa,直接操作散列表sidetable
	            ClearExclusive(&isa.bits);
	            if (rawISA()->isMetaClass()) return (id)this;
	            if (!tryRetain && sideTableLocked) sidetable_unlock();
	            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
	            else return sidetable_retain();
	        }
	        // don't check newisa.fast_rr; we already called any RR overrides
	        // dealloc源码
	        if (slowpath(tryRetain && newisa.deallocating)) {
	            ClearExclusive(&isa.bits);
	            if (!tryRetain && sideTableLocked) sidetable_unlock();
	            return nil;
	        }
	        
	        uintptr_t carry;
	        // 执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
	        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
	        // 判断extra_rc是否已满,carry是标识符
	        if (slowpath(carry)) {
	            // newisa.extra_rc++ overflowed
	            if (!handleOverflow) {
	                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();
	            sideTableLocked = true;
	            transcribeToSideTable = true;
	            // 如果extra_rc已满,则直接将满状态的一半拿出来存到extra_rc
	            newisa.extra_rc = RC_HALF;
	            // 给一个标识符为YES,表示需要存储到散列表
	            newisa.has_sidetable_rc = true;
	        }
	    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
	
	    if (slowpath(transcribeToSideTable)) {
	        // Copy the other half of the retain counts to the side table.
	        // 将另一半存在散列表的rc_half中,即满状态下是8位,一半就是1左移7位,即除以2
	        // 这样操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多
	        sidetable_addExtraRC_nolock(RC_HALF);
	    }
	
	    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
	    return (id)this;
	}
  • 散列表在内存中可以存在多张,那么它最多能够存在多少张呢?
    • 如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(针对整个表的读写),当开锁时,由于所有数据都存在于一张表中,这样就会有数据不安全的风险。
    • 如果每个对象都新开一个散列表,那样会非常消耗性能,因此也不能无限创建散列表。
  • 散列表的类型是 SideTable,有如下定义:
	struct SideTable {
	    spinlock_t slock;        // 开/解锁
	    RefcountMap refcnts;     // 引用计数表
	    weak_table_t weak_table; // 弱引用表
	    
	    ....
	}
  • 通过查看 sidetable_unlock 方法定位 SideTables,可以看到内部是通过 SideTablesMap 的 get 方法获取的,而 SideTablesMap 是通过 StripedMap 定义的,如下所示:
	void 
	objc_object::sidetable_unlock()
	{
	    // SideTables散列表并不只是一张,而是很多张,与关联对象表类似
	    SideTable& table = SideTables()[this];
	    table.unlock();
	}

	static StripedMap<SideTable>& SideTables() {
	    return SideTablesMap.get();
	}

	static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
  • 进入 StripedMap 的定义,在这里可以看出,同一时间,真机中散列表最多只能有 8 张。
	template<typename T>
	class StripedMap {
	#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
	    enum { StripeCount = 8 };
	#else
	    enum { StripeCount = 64 };
	#endif
	
	    struct PaddedT {
	        T value alignas(CacheLineSize);
	    };
	
	    PaddedT array[StripeCount];
	
	    static unsigned int indexForPointer(const void *p) {
	        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
	        // 通过哈希函数获取哈希下标
	        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
	    }
  • 那么为什么在内存管理中使用散列表,而不使用数组和链表呢?
    • 数组的特点是查询方便(即通过下标访问),但是增删比较麻烦(类似于 methodList,通过 memcopy、memmove 增删,非常麻烦),因此数据的特性是读取快,但存储不方便。
    • 链表的特点是增删方便,查询慢(需要从头节点开始遍历查询),因此链表的特性是存储快,读取慢。
    • 散列表的本质是一张哈希表,哈希表集合了数组和链表的长处,增删改查都比较方便。例如拉链哈希表是最常用的,如下所示:
      在这里插入图片描述
    • 可以从 SideTables -> StripedMap -> indexForPointer 中可以验证:散列表是“通过哈希函数计算哈希下标”以及“sideTables 为什么可以使用[]”的原因,如下所示:
	static unsigned int indexForPointer(const void *p) {
	        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
	        // 通过哈希函数计算哈希下标
	        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
	    }
	
	 public:
	 	// 重载操作符,sideTables 可以使用[]的原因
	    T& operator[] (const void *p) { 
	        return array[indexForPointer(p)].value; 
	    }
	    const T& operator[] (const void *p) const { 
	        return const_cast<StripedMap<T>>(this)[p]; 
	    }
  • 综上所述,retain 的底层实现流程:
    • 首先会判断是否是 Nonpointer isa,如果不是,则直接操作散列表进行 +1 操作;
    • 如果是 Nonpointer isa,则需要判断是否正在被释放,如果正在被释放,则执行 dealloc 流程,释放弱引用表和引用计数表,最后 free 释放对象内存;
    • 如果没有正在被释放,则对 Nonpointer isa 进行常规的引用计数+1。extra_rc 在真机上只有8位用于存储引用计数的值,当存储已满时,需要借助散列表用于存储,将已满的 extra_rc 对半分,一半(即27)存储在散列表中,另一半仍然存储在 extra_rc 中,用于常规的引用计数的 +1 或者 -1 操作,然后再返回。

在这里插入图片描述

② release
  • 通过 setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease 顺序,进入 rootRelease 源码,可以看到其操作与 retain 正好相反;
    • 判断是否是 Nonpointer isa,如果不是,则直接对散列表进行 -1 操作;
    • 如果是 Nonpointer isa,则对 extra_rc 中的引用计数值进行 -1 操作,并存储此时的 extra_rc 状态到 carry 中;
    • 如果此时的状态 carray 为 0,则执行 underflow 流程;
  • underflow 的执行流程如下:
    • 判断散列表中是否已存储一半的引用计数;
    • 如果是,则从散列表中取出存储的一半引用计数,进行 -1 操作,然后存储到 extra_rc 中;
    • 如果此时 extra_rc 没有值,并且散列表中也为空,则直接进行析构,即 dealloc 操作(自动触发)。
	ALWAYS_INLINE bool 
	objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
	{
	    if (isTaggedPointer()) return false;
	
	    bool sideTableLocked = false;
	
	    isa_t oldisa;
	    isa_t newisa;
	
	 retry:
	    do {
	        oldisa = LoadExclusive(&isa.bits);
	        newisa = oldisa;
	        // 判断是否是Nonpointer isa
	        if (slowpath(!newisa.nonpointer)) {
	            // 如果不是,则直接操作散列表-1
	            ClearExclusive(&isa.bits);
	            if (rawISA()->isMetaClass()) return false;
	            if (sideTableLocked) sidetable_unlock();
	            return sidetable_release(performDealloc);
	        }
	        // don't check newisa.fast_rr; we already called any RR overrides
	        uintptr_t carry;
	        // 进行引用计数-1操作,即extra_rc-1
	        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
	        // 如果此时extra_rc的值为0了,则走到underflow
	        if (slowpath(carry)) {
	            // don't ClearExclusive()
	            goto underflow;
	        }
	    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
	                                             oldisa.bits, newisa.bits)));
	
	    if (slowpath(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 (slowpath(newisa.has_sidetable_rc)) {
	        if (!handleUnderflow) {
	            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.
	            goto retry;
	        }
	
	        // 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.
	            // 进行-1操作,然后存储到extra_rc中
	            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
	            bool stored = StoreReleaseExclusive(&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.nonpointer) {
	                    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.
	        }
	    }
	    // 此时extra_rc中值为0,散列表中也是空的,则直接进行析构,即自动触发dealloc流程
	    // Really deallocate.
	    // 触发dealloc的时机
	    if (slowpath(newisa.deallocating)) {
	        ClearExclusive(&isa.bits);
	        if (sideTableLocked) sidetable_unlock();
	        return overrelease_error();
	        // does not actually return
	    }
	    newisa.deallocating = true;
	    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
	
	    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 的底层实现流程如下图所示:

在这里插入图片描述

以上是关于iOS之深入解析内存管理retain与release的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析内存管理Tagged Pointer的底层原理

iOS之深入解析内存管理NSTimer的强引用问题

iOS之深入解析内存管理的引用计数retainCount的底层原理

iOS之深入解析Memory内存

iOS之深入解析如何检测“循环引用”

iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理