ARC
- ARC在编译期和运行期做了什么?
- 编译期:
- 运行期:
- block 是如何在 ARC 中工作的?
- ARC的实现分析
- __strong
- 自己生成并持有
- storeStrong
- SideTable散列表
- objc_retain
- objc_release
- sidetable_release
- retainCount
- 非自己生成并持有
ARC在编译期和运行期做了什么?
ARC (Automatic Reference Counting)是Objective-C在iOS 5.0之后提供的一种自动内存管理机制。它帮助开发者管理应用程序的内存使用,减少了因为忘记释放内存导致的内存泄漏问题,以及过早释放内存引发的程序崩溃问题。ARC工作在编译期和运行期做了以下事情:
编译期:
- 自动插入
Retain
(引用计数+1)和Release
(引用计数-1)、Autorelease
(延迟引用计数-1)代码: 当对象被创建或引用传递时,引用计数+1;当对象不再使用时,ARC会自动插入释放内存的代码,从而使引用计数-1。如果发现在同一个对象上执行了多次“保留”与“释放”操作,那么ARC有时可以成对的移除这两个操作。 - 检查代码,如果发现明显的所有权违规问题或者循环引用,编译器会给出警告。编译器还会为你生成合适的dealloc方法。
- 使用ARC后,编译器会自动管理
autoreleasepool
,进行合理的创建和释放使内存达到稳定的状态,无需开发者手动管理。 - ARC更新
@property
属性的默认语义。像强(strong)
和弱(weak)
引用就是这种情况。强引用会自动增加对象的引用计数,而弱引用则不会。
运行期:
- 在运行阶段,根据对象的引用情况,自动调用
release
以及autorelease
,以减少或延迟引用计数。引用计数为0的对象会被立即释放。 - 通过对被引用对象的追踪,ARC能够自动破解一部分循环引用,例如:通过引入
weak
属性,它不会增加对象的引用计数,这样一个对象即使被另一个对象通过weak
引用,也能够被正确释放。 - 除释放对象之外,ARC还负责清空所有弱引用(weak reference)的值,阻止野指针的问题发生。
另外:ARC并不是内存管理的终极解决方案,它并不能处理所有情况。比如,如果代码中存在强循环引用
,即使采用了ARC也无法自动解决。在这种情况下,开发者需要找出并打破这种循环。所以,编程者仍然需要理解引用计数和内存管理的基本原理,合理地设计代码,避免循环引用的发生。
block 是如何在 ARC 中工作的?
在ARC下,编译器会根据情况自动将栈上的block复制到堆上,比如block作为函数返回值时,这样你就不必再调用
Block Copy
。
需要注意的一件事是,在ARC下,NSString * __block myString
这样写的话,block
会对NSString
对象强引用,而不是造成悬垂指针问题。如果你要和MRC保持一致,请使用__block NSString * __unsafe_unretained myString
或(更好的是)使用__block NSString * __weak myString
。
ARC的实现分析
__strong
自己生成并持有
id __strong obj0 = [[NSObject alloc] init];
NSLog(@"%@", obj0);
我们转成汇编之后发现整个的执行过程如下:
主要经历的方法如下:
//初始化的两个方法如下:
objc_alloc_init
objc_storeStrong
//所有程序执行完之后:
objc_autoreleasePoolPop
所以我们直接来看storeStrong方法。
storeStrong
在runtime文件中找到这个函数
如下
objc_storeStrong(id *location, id obj)
{
//用prev保留被赋值对象原来所指向的对象
id prev = *location;
//如果所赋的值和被赋值对象所指的对象是同一个,就直接return不进行任何操作
if (obj == prev) {
return;
}
//如果所赋的值和被赋值对象所指的对象不是同一个
//就先objc_retain使所赋的值对象的引用计数+1(因为赋值成功之后要持有)
objc_retain(obj);
//改变被赋值对象所指向的对象为新的对象
*location = obj;
//因为prev保留了被赋值对象原来所指向的对象,所以对prev进行objc_release使原来的旧对象引用计数-1,因为现在我们的被赋值对象已经不指向它了
objc_release(prev);
}
例子:(赋值操作时)
obj = otherObj;
//会变成如下函数调用
objc_storeStrong(&obj, otherObj);
其中做了四件事:
- 检查输入的 obj 地址 和指针指向的地址是否相同。
- 持有对象,引用计数 + 1 。
- 指针指向 obj。
- 原来指向的对象引用计数 - 1(释放对象)。
对于这里传入的NULL来说这就等同于向对象发送release消息
详细逻辑思路见上方代码中注释讲解。
SideTable散列表
内存管理主要结构代码
struct SideTable {
spinlock_t slock; // 保证原子操作的自旋锁
RefcountMap refcnts; // 引用计数的 hash 表
weak_table_t weak_table; // weak 引用全局 hash 表
};
objc_retain
来学习一下objc_object的具体实现
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
再接着看最后所调用的retain()
方法:
objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
系统会对是否支持NONPOINTER_ISA进行不同的处理
每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。
union isa_t
{
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;//->表示使用优化的isa指针
uintptr_t has_assoc : 1;//->是否包含关联对象
uintptr_t has_cxx_dtor : 1;//->是否设置了析构函数,如果没有,释放对象更快
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针
uintptr_t magic : 6;//->固定值,用于判断是否完成初始化
uintptr_t weakly_referenced : 1;//->对象是否被弱引用
uintptr_t deallocating : 1;//->对象是否正在销毁
uintptr_t has_sidetable_rc : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1,未溢出的时候为0
uintptr_t extra_rc : 19; //->存储引用计数
};
};
支持Nonpointer isa的处理
objc_object::rootRetain()
{
return rootRetain(false, false);
}
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)) {
// 2、SideTable散列表方法
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
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++
// 如果newisa.extra_rc++ 溢出, carry==1
if (slowpath(carry)) {
// 溢出
if (!handleOverflow) {
ClearExclusive(&isa.bits); // 空操作(系统预留)
return rootRetain_overflow(tryRetain);// 再次调用rootRetain(tryRetain,YES)
}
// 执行rootRetain_overflow会来到这里,就把extra_rc对应的数值(一半)存到SideTable
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF; // 溢出了,设置为一半,保存一半到SideTable
newisa.has_sidetable_rc = true; // 标记借用SideTable存储
}
// StoreExclusive保存newisa.bits到isa.bits,保存成功返回YES
// 这里while判断一次就结束了
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// 借位保存:把RC_HALF(一半)存入SideTable
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
不支持的处理
objc_object::rootRetain()
{
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
// 判断是否溢出,溢出了就是引用计数太大了,不管理了
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
// 引用计数加1,(上图可知,偏移2位,SIDE_TABLE_RC_ONE = 1<<2)
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
可以看到不支持Nonpointer isa
的处理就是直接sidetable_retain
,这是由于计数都存储在sidetable中了,处理逻辑较支持Nonpointer isa的情况要简单一些。
不支持Nonpointer isa 的处理:
去sidetable取出计数信息 执行加一操作
支持Nonpointer isa的处理:
- 首先判断是否为标签指针类型 如果是 直接返回
- 进入do-while处理逻辑
- 先判断是否为 其一定支持
Nonpointer isa
的架构,但是isa
没有额外信息
如果没有额外信息 那就和不支持意义一样(判断是否有优化) 引用计数存储在sidetable
中,走sidetable
的引用计数+1的流程 - 判断对象是否正在释放,如果正在释放则执行
dealloc
流程。 - 有存储额外信息,包含引用计数。我们尝试对
isa
中的extra_rc++
加一进行测试
3.1 如果没有溢出越界的情况,我们将isa的值修改为extra_rc++
之后的值
3.2 如果有溢出 将一半的计数存储到extra_rc
,另一半存储到sidetable
中去 设置设置标志位位true
retain的总结:
如果isa
可以存储额外信息,那么有extra_rc
位用来存储引用计数,当引用计数满了之后 就会存储到sidetable
中 。
retain
的流程也是针对isa
是否支持存储信息分别进行处理
当extra_rc
存储溢出了,这个时候是一半(extra_rc
能表示的最大值+1的一半)在extra_rc
一半存储在sidetable
中,这里跟release
的操作是对应的(extra_rc
不够减了也是去sidetable
借extra_rc
最大值的一半的计数),这样设计的好处避免了频繁的去sidetable
中读取计数信息—假如我们溢出了把计数全部存到sidetable
中去,那么有release
的时候,extra_rc
也不够减了,又去借,这就大大降低了效率,比起直接操作isa
。
这个优化的好处就是我们省去了频繁去sidetable
中读取计数信息,从而大大提高了效率,这样的话因为平时绝大多处操作都是普通的retain
和release
,所以都可以得到优化,而我们如果要读取引用计数的值的话就相对麻烦一点,需要sidetable
和extra_rc
两者相加,但是读取引用计数值的方法使用率几乎为零,也就是调试的时候会用到而已。
objc_release
来学习一下objc_release
的具体实现
objc_release(id obj)
{
if (obj->isTaggedPointerOrNil()) return;
return obj->release();
}
下面我们看一下真正的release
方法:
// handleUnderflow 参数看似是一个 bool 类型的表示是否处理下溢出,
// 当溢出发生了的话是必须要处理的,如果 handleUnderflow 为 false,
// 那么它会借一个 rootRelease_underflow 函数,并再次调用 rootRelease 函数,
// 并把 handleUnderflow 参数传递 true。
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
// 如果是 Tagged Pointer 直接返回 false,Tagged Pointer 不参与引用计数处理,它内存位于栈区,由系统处理
if (isTaggedPointer()) return false;
// 标记 SideTable 是否加锁了
bool sideTableLocked = false;
// 临时变量存放旧的 isa
isa_t oldisa;
// 临时变量存放字段修改后的 isa
isa_t newisa;
retry:
do {
// 以原子方式读到 &isa.bits 的数据
oldisa = LoadExclusive(&isa.bits);
// 把 oldisa 赋值给 newisa,此时 isa.bits/oldisa/newisa 三者是相同的
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
// 如果对象的 isa 只是原始指针 (Class isa/Class cls)
// __arm64__ && !__arm642__ 平台下,取消 &isa.bits 的独占访问标记
// x86_64 下什么都不需要做,对它而言上面的 LoadExclusive 也只是一个原子读取 (atomic_load)
ClearExclusive(&isa.bits);
// 如果当前对象是元类对象,则直接返回 false
if (rawISA()->isMetaClass()) return false;
// 如果当前 SideTable 加锁了则进行解锁
if (sideTableLocked) sidetable_unlock();
// 只针对 isa 是原始 Class cls 的对象调用的 sidetable_release 函数
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
// 不要检查 newisa.fast_rr; 我们之前已经调用过所有 RR 的重载
// extra_rc--
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
// 如果发生了下溢出的话,要进行处理,如果没有发生的话就是结束循环,解锁并执行 return false;
if (slowpath(carry)) {
// don't ClearExclusive()
// 不执行 ClearExclusive()
// 这里直接 goto 到 underflow 中去处理溢出
goto underflow;
}
// 这里结束循环的方式同 rootRetain 函数,都是为了保证 isa.bits 能正确修改
// StoreExclusive 和 StoreReleaseExclusive 的区别在于 memory_order_relaxed 和 memory_order_release
// 可参考 https://en.cppreference.com/w/cpp/atomic/memory_order
// 当 &isa.bits 与 oldisa.bits 相同时,把 newisa.bits 复制给 &isa.bits,并返回 true
// 当 &isa.bits 与 oldisa.bits 不同时,
// 把 oldisa.bits 复制给 &isa.bits, 并返回 false (此时会继续进行 do wehile 循环)
} while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits)));
// 如果未下溢出的话,不需要 goto underflow,如果 Sidetable 加锁了,
// 则进行解锁,然后返回 false,函数执行结束
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// newisa.extra_rc-- 发生溢出时,有两种方式进行处理:
// 1. 如果 SideTable 中有保存对象的引用计数的话可以从 SideTable 中借用
// 2. 如果 SideTable 中没有保存对象的引用计数的话,表示对象需要执行销毁了
// abandon newisa to undo the decrement
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) {
// 如果 newisa.has_sidetable_rc 为 true,表示在 SideTable 中有保存对象的引用计数
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
// 如果 handleUnderflow 为 false,则调用 rootRelease_underflow,“递归” 调用 rootRelease 处理溢出
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
// 将 retain count 从 SideTable 中转移到 isa.extra_rc 中保存。
if (!sideTableLocked) {
// 如果 SideTable 未加锁
// 同上,清除独占标记
ClearExclusive(&isa.bits);
// 给 SideTable 加锁
sidetable_lock();
// 并把加锁标记置为 true
sideTableLocked = true;
// Need to start over to avoid a race against the nonpointer -> raw pointer transition.
// 回到 retry
goto retry;
}
// Try to remove some retain counts from the side table.
// 尝试从 SideTable 中移除一些引用计数。
// 是从 SideTable 借一些引用计数出来,borrowed 是借到的值,可能是 0,也可能是 RC_HALF
// refcnts 中保存的引用计数是 RC_HALF 的整数倍,
// 每次 retain 溢出时都是往 refcnts 中转移 RC_HALF,
// 剩下的 RC_HALF 放在 extra_rc 字段中
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.
// 为了避免竞态,即使 SideTable 计数现在为零,也必须保持 has_sidetable_rc 之前的设置。
if (borrowed > 0) {
// borrowed 表示从 SideTable 借到引用计数了
// Side table retain count decreased.
// SideTable 引用计数 减少。
// Try to add them to the inline count.
// 尝试将借来的引用计数增加到 extra_rc 中。
// 赋值。(包含减 1 的操作)
newisa.extra_rc = borrowed - 1; // redo the original decrement too
// 原子保存修改后的 isa.bits
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// 如果失败的话
// Inline update failed.
// extra_rc 更新失败。
// Try it again right now.
// This prevents livelock on LL/SC architectures where the side
// table access itself may have dropped the reservation.
// 立即进行重试。
// 这样可以防止在 LL/SC体系结构上发生 livelock,在这种情况下 SideTable 访问本身可能已取消预留。
// 活锁可参考: https://www.zhihu.com/question/20566246
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
// 把借来的引用计数增加到 extra_rc 中
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
// 如果还是失败的话,下面 goto retry 再重来
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// 如果还是失败了。
// Inline update failed.
// Put the retains back in the side table.
// 把从 SideTable 借来的引用计数还放回到 SideTable 中去。
sidetable_addExtraRC_nolock(borrowed);
// 然后直接 goto retry; 进行全盘重试
goto retry;
}
// Decrement successful after borrowing from side table.
// 减去从 SideTable 借来的引用计数成功。
// 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();
// 返回 false
return false;
}
else {
// SideTable 是空的,执行 dealloc 分支
// Side table is empty after all. Fall-through to the dealloc path.
}
}
// Really deallocate.
// 执行销毁。
if (slowpath(newisa.deallocating)) {
// 如果对象已经被标记了正在执行释放...
// 这里又进行释放,明显是发生了过度释放...
// 清除独占标记
ClearExclusive(&isa.bits);
// 如果 SideTable 加锁了则进行解锁
if (sideTableLocked) sidetable_unlock();
// 调用 overrelease_error,crash 报错...
// 对象在销毁的过程中过度释放;中断 objc_overrelease_during_dealloc_error 进行调试
return overrelease_error();
// does not actually return
}
// 把对象的 isa 的 deallocating 置为 true。isa 的又一个字段被设置了,越来的越多的字段被发现设置位置了。
newisa.deallocating = true;
// 设置 &isa.bits,如果失败,则 goto retry;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
// 如果加锁了,则进行解锁。
if (slowpath(sideTableLocked)) sidetable_unlock();
// 这个函数以当前的水平实在是看不懂呀...
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
// 如果 performDealloc 为 true,则以消息发送的方式调用 dealloc
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
}
sidetable_release
// return uintptr_t instead of bool so that the various raw-isa -release paths all return zero in eax
// 返回 uintptr_t 而不是 bool,以便各种 raw-isa -release路径在 eax 中都返回零
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
// 如果当前平台支持 isa 优化
#if SUPPORT_NONPOINTER_ISA
// 如果 isa 是优化的 isa 则直接执行断言,
// sidetable_release 函数只能在对象的 isa 是原始 isa 时调用(Class cls)
ASSERT(!isa.nonpointer);
#endif
// 从全局的 SideTalbes 中找到 this 所处的 SideTable
SideTable& table = SideTables()[this];
// 临时变量,标记是否需要执行 dealloc
bool do_dealloc = false;
// 加锁
table.lock();
// it 的类型是: std::pair<DenseMapIterator<std::pair<Disguised<objc_object>, size_t>>, bool>
// try_emplace 处理两种情况:
// 1. 如果 this 在 refcnts 中还不存在,则给 this 在 buckets 中找一个 BucketT,
// KeyT 放 this, ValueT 放 SIDE_TABLE_DEALLOCATING,然后使用这个 BucketT 构建一个 iterator,
// 然后用这个 iterator 和 true 构造一个 std::pair<iterator, true> 返回。
// 2. 如果 this 在 refcnts 中已经存在了,则用 this 对应的 BucketT 构建一个 iterator,
// 然后用这个 iterator 和 false 构造一个 std::pair<iterator, false> 返回。
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
// refcnt 是引用计数值的引用。
// it.first 是 DenseMapIterator,它的操作符 -> 被重写了返回的是 DenseMpaIterator 的 Ptr 成员变量,
// 然后 Ptr 的类型是 BucketT 指针,
// 然后这里的 ->second 其实就是 BucketT->second,其实就是 size_t,正是保存的对象的引用计数数据。
auto &refcnt = it.first->second;
if (it.second) {
// 如果 it.second 为 true,表示 this 第一次放进 refcnts 中,且 BucketT.second 已经被置为 SIDE_TABLE_DEALLOCATING,
// 标记为需要执行 dealloc
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
// 如果 refcnt < SIDE_TABLE_DEALLOCATING,那可能的情况就是 SIDE_TABLE_WEAKLY_REFERENCED 或者为 0
// 标记为需要执行 dealloc
do_dealloc = true;
// 与 SIDE_TABLE_DEALLOCATING 执行或操作,表示把 refcnt 标记为 DEALLOCATING
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
// refcnt & SIDE_TABLE_RC_PINNED 值为 false 的话表示,
// rcfcnts 中保存的 this 对应的 BucketT 的 size_t 还没有溢出,还可正常进行操作存储 this 的引用计数
// refcnt 减去 SIDE_TABLE_RC_ONE
refcnt -= SIDE_TABLE_RC_ONE;
}
// 解锁
table.unlock();
if (do_dealloc && performDealloc) {
// 如果 do_dealloc 被标记为需要 dealloc 并且入参 performDealloc 为 true,
// 则以 objc_msgSend 消息发送的方式调用对象的 dealloc 方法
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
在上面的代码当中有两个宏定义:
现在release
也很好理解了:
- 依旧是判断是否为
taggedPointer
,如果是,直接返回false
,不需要dealloc
- 判断是否有优化 如果没有 就直接操作散列表,使引用计数-1
- 判断是引用计数为否为0 如果是0则执行
dealloc
流程 - 若
isa
有优化,则对象的isa
位存储的引用计数减一,且通过carry判断是否向下溢出了 结果为负数(下图有点问题 应该是判断是有向下溢出),如果是,如果到-1 就放弃newisa
改为old
,并将散列表中一半引用计数取出来,然后将这一半引用计数减一在存到isa
的extra_rc
- 如果
sidetable
的引用计数为0,对象进行dealloc
流程
其实和retain
一样 不过release
操作变成-1 并且需要注意从sidetable
中的一半减一放入
retainCount
如果对象的isa
是非指针的话,引用计数同时在 extra_rc
字段和 SideTable
中保存,要求它们的和。如果对象的isa
是原始isa
的话,对象的引用计数数据只保存在 SideTable
中。
- 当对象的
isa
经过优化,首先获取isa
位域extra_rc
中的引用计数,默认会+1(防止你没持有就要打印),uintptr_t rc = 1 + bits.extra_rc;然后获取散列表的引用计数表中的引用计数,两者相加得到对象的最终的引用计数 - 当对象的
isa
没有经过优化,则直接获取散列表的引用计数表中的引用计数,返回。 - 当我们
alloc
一个对象时,然后调用retainCount
函数,得到对象的引用计数为1。这是因为在底层rootRetainCount
方法中,引用计数默认+1了,这里只有对引用计数的读取操作,是没有写入操作的,简单来说就是:为了防止alloc
创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc
中的引用计数仍然为0(因为extra_rc
中存放的引用计数值是除该对象本身之外的引用计数数量)
所以 通过alloc
或者new
这样赋值来新建一个对象 ARC MRC环境下都是1 这个1是底层默认的返回值加一 没有调用retain
其他强引用 才会调用objc_retain
来持有
从runtime
源码中找到retainCount
供大家参考:
inline uintptr_t
objc_object::rootRetainCount()
{
// 如果是 Tagged Pointer 的话,获取它的引用计数则直接返回 (uintptr_t)this
if (isTaggedPointer()) return (uintptr_t)this;
// 加锁
sidetable_lock();
// 以原子方式加载 &isa.bits 数据
isa_t bits = LoadExclusive(&isa.bits);
// 如果是 __arm64__ && !__arm64e__ 平台下,要清除独占标记
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
// 如果对象的 isa 是非指针的话,引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和
// 这里加 1, 是因为 extra_rc 存储的是对象本身之外的引用计数的数量(这个加1操作也就是为什么我们新alloc等初始化一个对象之后,打印它的引用计数值为1)
uintptr_t rc = 1 + bits.extra_rc;
// 如果 has_sidetable_rc 位为 1,则表示在 SideTable 中也保存有对象的引用计数数据
if (bits.has_sidetable_rc) {
// 找到对象的在 SideTable 中的引用计数并增加到 rc 中
rc += sidetable_getExtraRC_nolock();
}
// 解锁
sidetable_unlock();
// 返回 rc
return rc;
}
sidetable_unlock();
// 如果对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中
return sidetable_retainCount();
}
isa如果优化过,即支持Nonpointer isa,则在sidetable
中查找引用计数的函数如下:
size_t
objc_object::sidetable_getExtraRC_nolock()
{
// 此函数只限定 isa 是非指针的对象调用
ASSERT(isa.nonpointer);
// 从全局的 SideTables 中找到 this 所处的 SideTable
SideTable& table = SideTables()[this];
// 查找对象的引用计数
RefcountMap::iterator it = table.refcnts.find(this);
// 如果未找到,返回 0
if (it == table.refcnts.end()) return 0;
// 如果找到了做一次右移操作,后两位是预留的标记位
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
不支持Nonpointer isa的话,在sidetable
中查找引用计数的函数如下:
uintptr_t
objc_object::sidetable_retainCount()
{
// 找到 this 所在的 SideTable
SideTable& table = SideTables()[this];
// refcnt_result 初始为 1,因为 SideTable 中存储的是对象本身之外的引用计数的数量
size_t refcnt_result = 1;
// 加锁
table.lock();
// 在 refcnts 中查找对象的引用计数
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
// 这也对 SIDE_TABLE_RC_PINNED 有效
// 移位并增加到 refcnt_result
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
// 解锁
table.unlock();
return refcnt_result;
}
retainCount
相关流程图如下:
在学完了release
和retain
之后,我们浅浅地总结一下: 在我们alloc
初始化完一个对象的过程中,系统在编译阶段,程序底层默认对对象进行了引用计数+1操作,但是这个1不会出现在sidetable
中,也不会出现在extra_rc
中,因为sidetable
和extra_rc
当中存放的都是该对象本身之外的引用计数的数量,所以初始状态sidetable
和extra_rc
中的值都是0,然后我们后续进行的retain
和release
操作都是针对sidetable
和extra_rc
中的引用计数进行+1或-1。
非自己生成并持有
id __strong obj = [NSMutableArray array];
NSLog(@"%@", obj);
我们发现出现了objc_retainAutoreleasedReturnValue
这个方法
接着我们就探究其原理:
先看一个例子:
@autoreleasepool {
__autoreleasing NSObject *obj = [NSObject new];
}
该代码对应的伪代码是:
// 获取哨兵POOL_SENTINEL
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
{
__autoreleasing NSObject *obj = [NSObject new];
}
// 就是release哨兵之后的autorelease对象。
objc_autoreleasePoolPop(atautoreleasepoolobj);
autorelease调用栈如下:
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└─ id objc_object::rootAutorelease2()
└─ static id AutoreleasePoolPage::autorelease(id obj)
└─ static id AutoreleasePoolPage::autoreleaseFast(id obj)
├─ id *add(id obj)
├─ static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └─ id *add(id obj)
└─ static id *autoreleaseNoPage(id obj)
├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└─ id *add(id obj)
一个autorelease对象在什么时刻释放?
答案是:
- 手动指定
Autoreleasepool
:当前Autoreleasepool
作用域大括号结束时释放; - 不手动指定:
autorelease
对象会被添加到最近一次创建的autoreleasepool
中,并在当前的runloop
迭代结束时候释放。
例如: 主Runloop
对Autoreleasepool
管理的流程:
Runloop
中,检测到触摸事件,创建事件,创建Autoreleasepool
,autorelease
对象加入pool
中,事件完成,Runloop
运行循环将要结束,释放Autoreleasepool
,向pool
中对象发送release
消息,Runloop
休眠。
autorelease 进行的非持有方法的优化(自动添加到自动释放池):
alloc/new/copy/mutableCopy
—持有对象方法会自动添加到自动释放池- 其他类方法返回的对象,如下面的
createObject
就会自动添加到自动释放池
@implementation ObjectTest
+ (instancetype)createObject {
return [self new];
}
接着我们来看这两个方法本尊:
id objc_autoreleaseReturnValue(id obj)
{
// prepareOptimizedReturn判断是否可以TSL优化,可以则标记,YES--就不需要调用 objc_autorelease(),优化性能
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
id objc_retainAutoreleasedReturnValue(id obj)
{
// 如果之前 objc_autoreleaseReturnValue() 存入的标志位为 ReturnAtPlus1,则直接返回对象,无需调用 objc_retain(),优化性能
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
return objc_retain(obj);
}
然后是这两个方法中if判断的条件调用的函数:
static ALWAYS_INLINE bool
prepareOptimizedReturn(ReturnDisposition disposition)
{
//获取返回标记
assert(getReturnDisposition() == ReturnAtPlus0);
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
if (disposition) setReturnDisposition(disposition);
return true;
}
return false;
}
static ALWAYS_INLINE ReturnDisposition
acceptOptimizedReturn()
{
ReturnDisposition disposition = getReturnDisposition();
setReturnDisposition(ReturnAtPlus0); // reset to the unoptimized state
return disposition;
}
TLS 全称为 Thread Local Storage,是每个线程专有的键值存储:
/*在某个线程上的函数调用栈上相邻两个函数对 TLS 进行了存取,这中间肯定不会有别的程序『插手』。
所以 getReturnDisposition() 和 setReturnDisposition() 的实现比较简单,不需要判断考虑是针对哪个对象的 Disposition 进行存取,因为当前线程上下文中只处理唯一的对象,保证不会乱掉。 */
static ALWAYS_INLINE void
setReturnDisposition(ReturnDisposition disposition)
{
tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}
callerAcceptsOptimizedReturn(__builtin_return_address(0))
函数在不同架构的 CPU 上实现也是不一样的。 主要作用:
__builtin_return_address(0)获取当前函数返回地址,传入 callerAcceptsOptimizedReturn 判断调用方是否紧接着调用了 objc_retainAutoreleasedReturnValue
当判断调用方紧接着调用了 objc_retainAutoreleasedReturnValue 或者 objc_unsafeClaimAutoreleasedReturnValue
直接返回当前对象地址,而不执行retain与autorelease操作.
其作用就是得到函数的返回地址,0–表示返回当前函数的返回地址,1–表示返回当前函数的调用方的返回地址;
ARC 会视情况在调用方法时可能会添加 retain ,在方法内部返回时可能会添加 autorelease ,经过优化后很可能会抵消。
1、持有、无引用:
- (void)test {
[BBObject new];
}
编译器编译后的伪代码:
- (void)test {
objc_release([BBObject new]) ;
}
2、持有、局部变量引用 __strong:
- (void)test {
__strong BBObject * obj = [BBObject new];
}
编译器编译后的伪代码:
- (void)test {
id temp = [BBObject new];
objc_storeStrong(&temp,nil);//相当于tmp指向对象执行release
}
3、持有、外部变量引用:
- (void)test {
self.obj = [BBObject new];
}
编译器编译后的伪代码:
- (void)test{
id temp = [BBObject new];
[self setObj:temp];//setter方法执行objc_storeStrong
objc_release(temp);
}
- (void)setObj:(id aObj) {
objc_storeStrong(&_obj, aObj);
}
4、不持有、无引用:
- (void)test {
[BBObject createObj];
}
编译器编译后的伪代码:
+ (instancetype) createObj {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease]
}
- (void)test {
objc_unsafeClaimAutoreleasedReturnValue([BBObject createObj]);
}
5、不持有、局部变量引用:
- (void)test {
BBObject * obj1 = [BBObject createObj];
}
编译器编译后的伪代码:
+ (instancetype) createObj {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease]
}
- (void)test {
id obj1 = objc_retainAutoreleasedReturnValue([BBObject createObj]);
objc_storeStrong(& obj1,nil);
}
发现obj1指向的对象不会加入autoreleasepool
6、不持有、外部变量引用:
+ (instancetype) createObj {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease]
}
- (void)test {
self.obj = [BBObject createObj];
}
编译后的伪代码:
- (void)test {
id temp = _objc_retainAutoreleasedReturnValue([Foo createFoo]);
[self setObj:temp]; // setter方法执行objc_storeStrong
objc_release(temp);
}
总结非自己生成并持有
objc_autoreleasedReturnValue
会检验调用者是否会对该对象执行retain
操作,如果会的话就不执行autorelease
,直接设置标志符ReturnAtPlus1objc_retainAutoreleaseReturnValue
在检验到标志符后,也不retain了(后面retain操作),直接返回对象本身,同样,如果检测到标识符显示后面没有retain操作,那么就走一遍retain使其引用计数加1
所以array这样的赋值新建一个对象,ARC环境下引用计数的1是底层默认的返回值加一 没有调用retain
其他强引用,才会调用objc_retain
来使引用计数加一。objc_retain
是retainAutoreleaseReturnValue
调用的。
一个问题:为什么要传入(NSError **)这种类型的参数
这是一个二级指针(指向指针的指针),将一个基本类型的变量通过函数参数传入函数内,在函数内如何改变都不会影响到外部变量的值,那如果我们要在函数内部改变外部变量的值,就应该将指针的值传入函数,然后函数中根据指针去找到指向的内存进行修改。
如果函数参数本身是一个对象,我们传入一个对象,对象本身就是一个地址(但是一个一级指针)。
如以下例子:
#import <Foundation/Foundation.h>
#import "StrongTest.h"
void test(StrongTest *obj) {
obj.name = @"3G Group";
//重新初始化obj,也就是改变参数obj的值(因为划分新的内存,对象的地址会变,而obj就是对象在内存中的地址)
obj = [[StrongTest alloc] init];
obj.name = @"iOS Club";
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
StrongTest *obj = [[StrongTest alloc] init];
obj.name = @"Xi You";
test(obj);
NSLog(@"obj.name = %@", obj.name);
}
return 0;
}
可以看到我们最后的打印结果中,并没有打印新初始化的对象的字符串。
这是因为我们使用alloc
init
后系统会在内存中新开辟一块存储空间存储一个新的对象,然后将函数中的obj
存储的指针值改为这个新的内存地址,而函数外的obj
并没有发生改变,还是指向原来的这个对象的地址
如果想在函数中改变函数外的对象,就需要用到二级指针,即指向指针的指针。
例子如下:
#import <Foundation/Foundation.h>
#import "StrongTest.h"
void test(StrongTest **obj) {
(*obj).name = @"3G Group";
*obj = [[StrongTest alloc] init];
(*obj).name = @"iOS Club";
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
StrongTest *obj = [[StrongTest alloc] init];
obj.name = @"Xi You";
test(&obj);
NSLog(@"obj.name = %@", obj.name);
}
return 0;
}
可以看到打印的结果就是我们新初始化后的对象中的字符串。
所以,所以对于NSError **
,我们可以在外面新建一个NSError
,当函数有错误时,新建一个NSError
对象并存储到我们新建的这个NSError
对象中。我们就可以通过判断NSError
是否为nil
来看函数运行是否出错。