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的底层原理