什么是自动引用计数
Objective-C 提供了两种应用程序堆空间内存管理方法,手动引用计数(MRC)和自动引用计数(ARC),区别在于 ARC 模式下,编译会插入适当的内存管理方法调用,也就是LLVM 编译器来进行内存管理。
在 LLVM 编译器中设置 ARC 为有效状态,就无需在使用 retain 和 release。 具体体现在 Xcode->Target->Build Settings-> Objective-C Automatic Reference Counting
尽管苹果会自动管理对象的引用计数,但是开发中仍然会有问题出现,例如循环引用,如下图:

为了更好的解决这些问题,有必要了解什么是引用计数以及引用计数是怎么实现的。
什么是引用计数
移动端的内存管理技术,主要有 GC(Garbage Collection,垃圾回收)的标记清除算法和苹果公司使用的引用计数方法。相比较于 GC 标记清除算法,引用计数法可以及时地回收引用计数为 0 的对象,减少查找次数。
引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。每一个对象都有一个关联的引用计数 —— 对该对象的活跃引用的数量。
引用计数内存管理的思考方式
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
Objecive-C 内存管理方法
在 Objective-C 中,关于引用计数的内存管理协议和方法,是由 Foundation
框架中的 NSObject
抽象基类提供的,如下表所示:
对象操作 | Objecive-C 方法 |
---|---|
生成对象 | alloc、new、copy |
持有对象 | retain |
释放对象 | release |
销毁对象 | dealloc |
通过上面的方法,实现了控制每一个对象关联的引用计数 retainCount
的增加或减少,如下图所示:

通过 objc 源码,可以了解 Objective-C 内存管理中使用的 alloc/retainCount/retain/release//dealloc
内部细节,从而理解苹果的引用计数原理。
alloc
在 objc-787.1 源码及以前可以找到 NSObject
类 的 alloc
方法,其核心函数为 _class_createInstanceFromZone
,做了以下几件事,可以发现 alloc
方法并未操作引用计数,通过isa结构分析 已经知道 isa
中存储的 extra_rc
是用于存储对象的引用计数。
-
计算开辟空间的大小
cls->instanceSize
,(16字节对齐) -
开辟内存空间
calloc
-
将指针与类进行绑定
obj->initInstanceIsa
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { ASSERT(!isTaggedPointer()); if (!nonpointer) { isa = isa_t((uintptr_t)cls); } else { ASSERT(!DisableNonpointerIsa); ASSERT(!cls->instancesRequireRawIsa()); isa_t newisa(0); # if SUPPORT_INDEXED_ISA ASSERT(cls->classArrayIndex() > 0); newisa.bits = ISA_INDEX_MAGIC_VALUE; // isa.magic is part of ISA_MAGIC_VALUE // isa.nonpointer is part of ISA_MAGIC_VALUE newisa.has_cxx_dtor = hasCxxDtor; newisa.indexcls = (uintptr_t)cls->classArrayIndex(); # else newisa.bits = ISA_MAGIC_VALUE; // isa.magic is part of ISA_MAGIC_VALUE // isa.nonpointer is part of ISA_MAGIC_VALUE newisa.has_cxx_dtor = hasCxxDtor; newisa.shiftcls = (uintptr_t)cls >> 3; # endif // This write must be performed in a single store in some cases // (for example when realizing a class because other threads // may simultaneously try to use the class). // fixme use atomics here to guarantee single-store and to // guarantee memory order w.r.t. the class index table // ...but not too atomic because we don't want to hurt instantiation isa = newisa; } }
但是,从 objc-818.2及以后,可以发现 initIsa
函数中增加了 extra_rc
的赋值为 1
,意味着初始化时引用计数值为 1,源码如下:
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
//...
newisa.extra_rc = 1;
}
retainCount
查看 NSObject
类的 retainCount
方法,最终调用核心函数为 rootRetainCount
。同样,由于 extra_rc
初始值不同,也是区分了 objc-787.1 和 objc-818.2。
-
objc-787.1
inline uintptr_t objc_object::rootRetainCount() { if (isTaggedPointer()) return (uintptr_t)this; sidetable_lock(); isa_t bits = LoadExclusive(&isa.bits); ClearExclusive(&isa.bits); if (bits.nonpointer) { uintptr_t rc = 1 + bits.extra_rc; if (bits.has_sidetable_rc) { rc += sidetable_getExtraRC_nolock(); } sidetable_unlock(); return rc; } sidetable_unlock(); return sidetable_retainCount(); }
-
objc 818.2
inline uintptr_t objc_object::rootRetainCount() { //... uintptr_t rc = bits.extra_rc; //... }
根据上面的源码,我们可以发现具体代码流程如下:
-
rootRetainCount
函数会先判断nonpointer
(也就是 isa 中是否有类信息) -
如果 isa 中有类信息,则先从 isa 中取出
extra_rc
值,之后判断散列表SideTable 中是否存储引用计数并相加。 -
如果 isa 中没有存储类信息,则直接调用
sidetable_retainCount
获取引用计数值。
两个 objc 版本不同点在于,objc-787.1 中会对结果进行 +1
操作。
retain
查看 NSObject
类的 retain
方法,最终调用核心函数为 rootRetain
。
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
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
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
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;
newisa.extra_rc = RC_HALF;
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.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
根据上面的源码,我们可以发现具体代码流程如下:
- 如果不是
nonpointer isa
,则直接对散列表sidetable
操作引用计数; - 否则,如果当前正在释放,则停止。
- 可以操作引用计数,则进行
extra_rc++
,对应代码newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry)
,RC_ONE
为1
; - 如果
extra_rc++
上溢出,则carry
有值,则处理extra_rc
最大值的一半RC_HALF
存储至散列表中,extra_rc
中保存另一半;并将has_sidetable_rc
置为true
。由于版本不同架构不同,extra_rc
最大值值可能也会不同,目前,可存储 8 位。
release
查看 NSObject
类的 release
方法,最终调用核心函数为 rootRelease
。
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;
if (slowpath(!newisa.nonpointer)) {
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;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
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.
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.
}
}
// Really deallocate.
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) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
根据上面的源码,我们可以发现具体代码流程如下:
-
如果不是
nonpointer isa
,则直接对散列表sidetable
操作引用计数; -
进行
extra_rc--
,对应代码为newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
-
如果此时
extra_rc
的下溢出,则走到underflow
,否则结束; -
判断
has_sidetable_rc
散列表中是否存储了一半的引用计数,从散列表中取出存储的一半引用计数,进行-1
操作,然后存储到extra_rc
中; -
如果
extra_rc
依旧下溢出,则直接自动触发dealloc
流程,deallocating
赋值为true
,发送dealloc
消息
dealloc
查看 NSObject
类的 dealloc
方法,最终调用核心函数为 rootDealloc
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
根据上面的源码,我们可以发现具体代码流程如下:
- 根据条件判断是否有
isa
、cxx
、关联对象、弱引用表、引用计数表,如果没有,则直接free
释放内存 - 如果有,则进入
object_dispose
方法
查看 object_dispose
id
object_dispose(id obj)
{
if (!obj) return nil;
//销毁实例而不会释放内存
objc_destructInstance(obj);
//释放内存
free(obj);
return nil;
}
👇
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//调用C ++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联引用
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}
return obj;
}
👇
inline void
objc_object::clearDeallocating()
{
//判断是否为nonpointer isa
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是,则直接释放散列表
sidetable_clearDeallocating();
}
//如果是,清空弱引用表 + 散列表
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
👇
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用计数
table.refcnts.erase(this);
}
table.unlock();
}
根据上面的源码,我们可以发现具体代码流程如下:
-
销毁实例
- 调用c++析构函数
- 删除关联引用
- 释放散列表
- 清空弱引用表
-
free释放内存
总结
-
在 objc-787.1 以前,
alloc
创建对象的引用计数为 0,会在编译时期,程序默认加 1,所以读取retainCount
时为 1,在 objc-787.1 以后,alloc
创建对象的引用计数为 1; -
retain
函数操作isa->bit->extra_rc++
,存储 8 位 ,超过最大值后会存储至散列表中; -
release
函数操作isa->bit->extra_rc--
,extra_rc
下溢出时,如果has_sidetable_rc
为true
,则取出 -1 赋值给extra_rc
;如果has_sidetable_rc
为false
则释放,调用dealloc
-
dealloc
会调用 c++析构函数、删除关联引用、释放散列表、清空弱引用表,最后释放内存
网友评论