【iOS】MRC&ARC
MRC
MRC是手动引用计数管理。通过retain、realease和autorelease等方法来控制对象的生命周期。
- 生成且持有对象:alloc/new/copy/mutableCopy等方法
- 持有对象:retain方法
- 释放对象:release方法
- 废弃对象:dealloc方法
对象内存管理
-
单个对象:
- 自己生成的对象自己持有。
- 非自己生成的对象自己也可以持有。
- 不再需要自己持有的对象释放。
- 非自己持有的对象无法释放。
-
多个对象:
- 一个对象不持有另外一个对象时,两个实例对象没有关联就可以将自己释放掉。
- 一个对象持有另外一个对象时,持有的对象需要在set方法中引用计数加一,释放时也需要将持有的对象销毁。
objc- (void)setMyObject:(MyObject*)myObj { [myObj retain]; // 引用计数+1 _myObj = myObj; } - (void)dealloc { [_myObj release]; [super dealloc]; // 持有的对象销毁 }- 一个对象持有并释放掉一个对象后持有另外一个对象时,则需要在更换原持有的对象前释放第一个对象。
- 一个对象持有一个对象并释放该对象后再次持有该对象时,需要在setter方法里判断是否重新赋值,避免野指针。
这是因为setter方法通常先release旧对象,再retain新对象,而重新持有同一个对象时,被释放的对象已经变成野指针,retain一个野指针会报错。
objc- (void)setMyObject:(MyObject*)myObj { if (_myObj != myObj) { [_myObj release]; [myObj retain]; _myObj = myObj; } }
@property修饰符
MRC下属性修饰符:
- assign:系统只会自动生成setter基础方法,不会生成内存管理代码。
- retain:系统会帮我们自动生成内存管理代码,但dealloc仍需要我们重写。
自动释放池
autorelease是一种支持引用计数的内存管理方式,主要用于延迟对象的释放。p autorelease操作会将对象注册到一个自动释放池中,当自动释放池被销毁时,池子里所有的对象都会做一次release操作。
注意:自动释放池是以栈形式管理,调用autoreleaase是将对象放到栈顶的自动释放池。
p autorelease操作只是把对象存入当前线程栈顶的自动释放池,而不是直接修改引用计数,因此{}内retainCount不会立刻改变。只有当自动释放池销毁时,才引用计数-1。
-
创建:
-
使用NSAutoreleasePool:
objc// 创建自动释放池 NSAutoreleasePoo *pool = [[NSAutoreleasePool alloc] init]; [pool release]; // 销毁自动释放池 [pool drain]; -
使用@autoreleasepool:
objc@autoreleasepool { // 开始创建自动释放池 } // 结束销毁自动释放池
-
-
使用:对象只有在创建autoreleasepool后才可以调用autorelease方法。
以使用@autoreleasepool创建的自动释放池为例。
objc
@autoreleasepool
{
Person *p = [[Person new] autorelease];
}
- 注意事项:
-
自动释放池中不要放占用内存较大的对象,如大量循环导致内存暴涨造成短时间内内存峰值上升。这是因为自动释放池是延迟释放机制,只有清空池子时才会将注册到池子的对象销毁。
objc@autoreleasepool { for (int i = 0; i < 99999; ++i) { Person *p = [[[Person alloc] init] autorelease]; } } // 上述代码可能会造成内存暴涨,优化为如下: for (int i = 0; i < 99999; ++i) { @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } } -
不要连续调用autorelease或调用autorelease后又调用release。这样都会给已经销毁的对象发送release消息,造成野指针崩溃,过度释放。
-
ARC
ARC是自动引用计数。使用ARC,开发者不再需要手动retain/release/autorelease。编译器会自动实现引用计数管理。
但ARC并不是管理所有内存,而是能够被编译器识别为OC对象的指针类型,包括:
- block
- OC对象,如id、NSError等
- 通过
__attribute__((NSObject))标记的类型
__attribute__((NSObject))是Clang编译器提供的一个扩展语法,用来给类型添加额外信息的标记。
而对于C指针、结构体、malloc内存等非OC对象,ARC均不负责。
ARC工作原理
ARC工作在编译期。编译源码时,编译器会分析源码中每个对象的生命周期,然后基于这些生命周期来添加相应的引用计数操作代码。
主要依赖于三个方法:
- retain:增加引用计数。
- release:降低引用计数,引用计数为0时释放对象。
- autorelease:在当前的autoreleasepool结束后,减少引用计数。
retain
objc
inline id
objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
核心步骤:先检查是否是TaggedPointer标签指针,不是的话再检查对象是否使用自定义retain/release方法。有就使用objc_msgSend发送retain消息到对象;反之就调用rootRetain()函数,直接引用计数,避免消息发送的开销。
objc
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
// 处理TaggedPointer
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
// 独占加载
oldisa = LoadExclusive(&isa().bits);
// 处理objc_retain,支持Swift对象内存管理、自定义retain消息转发
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);
// Swift对象特殊路径
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
// OC自定义retain,走消息转发
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;
}
}
#if !ISA_HAS_INLINE_RC
// No need for a CAS loop in this case; we aren't changing the ISA pointer
// 如果不支持isa内联引用计数,直接使用SideTable,无CAS循环
ClearExclusive(&isa().bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
#else
// 否则进入自旋循环
do {
transcribeToSideTable = false;
newisa = oldisa;
// 无优化isa,无内联extra_rc
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa().bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// don't check newisa.fast_rr; we already called any RR overrides
// 如果对象正在释放,禁止retain
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa().bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
// 优化isa技术,原子增加内联引用计数extra_rc
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
// extra_rc溢出
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
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.
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) {
// 把溢出的写入SideTable
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);
}
#endif
return (id)this;
}
核心步骤:
- 先判断是否是FastOrMsgSend,检测Swift等特殊处理。
- 再判断是否是元类。是的话直接返回对象,不处理引用计数。
- 再判断平台是否支持extra_rc内联引用计数。不支持的话直接操作SideTable管理引用计数。
- 支持的话进入CAS自旋循环:
- isa未优化技术直接使用SideTable管理引用计数。
- 对象正在dealloc则返回nil,retain失败。
- 采用了isa优化指针技术,则extra_rc+1。
- 检测溢出。无溢出直接更新isa,有溢出则同步更新SideTable。
CAS:比较并交换(Compare And Swap),属于乐观锁,无锁原子操作。先读取内存预期旧值,当内存当前值等于旧值时,原子更新为新值。
release
与retain类似。
objc
inline void
objc_object::release()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
objc
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false; // 如果是标记指针,直接返回false,因为标记指针有自己的内存管理。
bool sideTableLocked = false; // 初始化侧边表锁定状态为未锁定。
isa_t oldisa; // 存储对象的原始ISA信息。
isa_t newisa; // 存储新的ISA信息,用于修改。
retry:
do {
oldisa = LoadExclusive(&isa.bits); // 使用LoadExclusive获取ISA的bits字段,开始独占访问。
newisa = oldisa; // 复制旧的ISA信息到新ISA中,准备修改。
if (slowpath(!newisa.nonpointer)) { // 如果有侧边表。
ClearExclusive(&isa.bits); // 清除独占状态。
if (sideTableLocked) sidetable_unlock(); // 如果侧边表已锁定,解锁。
return sidetable_release(performDealloc); // 调用侧边表的release方法。
}
// 以下处理没有侧边表的情况。
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // 原子递减引用计数,并检查是否产生借位。
if (slowpath(carry)) { // 如果产生了借位,即发生了下溢。
// 不ClearExclusive,保留独占访问状态。
goto underflow; // 跳转到underflow标签,处理下溢情况。
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits))); // 使用StoreReleaseExclusive尝试更新ISA的bits字段,如果更新失败,继续循环。
if (slowpath(sideTableLocked)) sidetable_unlock(); // 如果侧边表已锁定,解锁。
return false; // 如果没有下溢,直接返回false。
underflow:
// 发生了下溢:从侧边表借用引用计数或析构对象。
// abandon newisa以撤销递减操作。
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) { // 如果有侧边表引用计数。
if (!handleUnderflow) { // 如果不处理下溢。
ClearExclusive(&isa.bits); // 清除独占状态。
return rootRelease_underflow(performDealloc); // 调用处理下溢的函数。
}
// 从侧边表转移引用计数到内联存储。
if (!sideTableLocked) { // 如果侧边表未锁定。
ClearExclusive(&isa.bits); // 清除独占状态。
sidetable_lock(); // 锁定侧边表。
sideTableLocked = true; // 设置侧边表锁定状态为已锁定。
goto retry; // 重新开始循环,防止竞态条件。
}
// 尝试从侧边表移除一些引用计数。
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); // 从侧边表借入一半的引用计数。
if (borrowed > 0) { // 如果成功借入。
// 侧边表引用计数减少。
// 尝试将它们添加到内联计数。
newisa.extra_rc = borrowed - 1; // 重新递减引用计数。
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits); // 尝试更新ISA的bits字段。
if (!stored) { // 如果更新失败。
// 内联更新失败。
// 将借来的引用计数放回侧边表。
sidetable_addExtraRC_nolock(borrowed); // 将引用计数放回侧边表。
goto retry; // 重新开始循环。
}
// 递减成功后从侧边表借用。
// 这个递减不可能是析构递减 - 侧边表锁和has_sidetable_rc标志确保如果所有其他线程都试图在我们工作时-release,最后一个会阻塞。
sidetable_unlock(); // 解锁侧边表。
return false; // 返回false。
}
else {
// 侧边表为空。
}
}
// 真正析构对象。
if (slowpath(newisa.deallocating)) { // 如果对象正在析构中。
ClearExclusive(&isa.bits); // 清除独占状态。
if (sideTableLocked) sidetable_unlock(); // 如果侧边表已锁定,解锁。
return overrelease_error(); // 返回overrelease错误。
}
newisa.deallocating = true; // 设置对象为正在析构中。
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; // 尝试更新ISA的bits字段,如果失败,重新开始循环。
if (slowpath(sideTableLocked)) sidetable_unlock(); // 如果侧边表已锁定,解锁。
__sync_synchronize(); // 确保所有线程可见状态的更新。
if (performDealloc) { // 如果需要执行析构。
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); // 发送-dealloc消息。
}
return true; // 返回true,表示对象应该被析构。
}
核心逻辑分为四部分:对象是否标记指针、对象是否有侧边表、引用计数是否会溢出、通过SideTable或析构处理溢出。
自动释放池
在ARC规则下,alloc/init/new/copy/mutableCopy开头的方法返回持有对象,方法内部不会调用autorelease。
我们可以通过重写dealloc方法检测是否是autorelease对象。如果对象离开作用域立刻触发dealloc销毁,说明不是autorelease对象;反之,离开作用域没有释放,等自动释放池释放才执行dealloc,则说明是autorelease对象。
对于Cocoa框架来说,可通过两种方式把对象放入autoreleasepool:
- NSAutoreleasePool:只能在MRC下使用
- @autoreleasepool {}:ARC和MRC下均可以使用
autorelease
objc
inline id
objc_object::autorelease()
{
if (isTaggedPointer()) return (id)this;
if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
objc
ALWAYS_INLINE id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
bool nonpointerIsa = false;
#if ISA_HAS_INLINE_RC
nonpointerIsa = isa().nonpointer;
// When we can cheaply determine if the object is deallocating, avoid
// putting it in the pool. Refcounting doesn't work on a deallocating object
// so it's pointless to put it in the pool, and potentially dangerous.
if (nonpointerIsa && isa().isDeallocating()) return (id)this;
#endif
// If the class has custom dealloc initiation, we also want to avoid putting
// deallocating instances in the pool even if it's expensive to check. (UIView
// and UIViewController need this. rdar://97186669)
if (!nonpointerIsa && ISA()->hasCustomDeallocInitiation() && rootIsDeallocating())
return (id)this;
if (prepareOptimizedReturn((id)this, true, ReturnAtPlus1)) return (id)this;
if (slowpath(isClass())) return (id)this;
return rootAutorelease2();
}
核心步骤:
- 先判断是否是Tagged Pointer。是的话直接返回,标签指针内置生命周期。
- 再调用prepareOptimizedReturn函数判断是否可以再当前调用栈深度+1的地方直接返回,从而跳过自动释放池的操作。
prepareOptimizedReturn主要用于某些优化场景下。例如当对象在当前作用域结束时就会被销毁,那么就没有必要将其放到自动释放池中延迟释放。
- 若以上两种情况都不适合,那么会调用autorelease操作核心实现函数rootAutorelease2()。
rootAutorelease2()
rootAutorelease2()是autorelease核心实现函数。负责将对象放入当前线程的自动释放池中,以便在作用域结束时释放对象。
objc
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
objc
public: static inline id autorelease(id obj)
{
// 断言确保传入的对象非空。
assert(obj);
// 确保对象不是标记指针,标记指针不需要自动释放。
assert(!obj->isTaggedPointer());
// 调用快速自动释放函数,尝试将对象加入当前热自动释放池页面。
id *dest __unused = autoreleaseFast(obj);
// 断言检查,确保dest要么是空,要么是空池占位符,要么指向obj。
// 这里用于验证autoreleaseFast是否正确执行。
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
// 返回原始对象,autorelease操作完成后对象仍然可用。
return obj;
}
objc
// 快速自动释放实现,尝试将对象加入到当前线程的热自动释放池页面。
static inline id *autoreleaseFast(id obj)
{
// 获取当前线程的热自动释放池页面。
AutoreleasePoolPage *page = hotPage();
// 如果页面存在并且没有满,则尝试将对象加入到页面中。
if (page && !page->full()) {
return page->add(obj);
}
// 如果页面存在但已满,调用特殊处理函数处理满页情况。
else if (page) {
return autoreleaseFullPage(obj, page);
}
// 如果页面不存在,调用无页面自动释放处理函数。
else {
return autoreleaseNoPage(obj);
}
}
- autoreleaseFast:首选快速路径。优先使用当前线程热页,页面存在且未满,直接写入,性能最高。
- autoreleaseFullPage:热页已满,新建AutoreleasePoolPage,链接到双向链表,更新hotPage,再add对象。
- autoreleaseNoPage:线程尚未创建任何释放池页面,创建第一个页面。
hotPage热页:当前线程正在使用、用来接收新增autorelease对象的页面。
objc
// 自动释放池页面的添加方法,用于向页面中添加一个对象。
id *AutoreleasePoolPage::add(id obj)
{
// 确保页面没有满。
assert(!full());
// 临时解除保护,允许修改页面。
unprotect();
// 记录下一个要写入的位置,这里使用next指针。
// 注意返回的是next-1的地址,但通过直接返回next避免了指针偏移。
id *ret = next;
// 将对象写入到next指向的位置,然后递增next。
*next++ = obj;
// 重新保护页面,防止其他线程修改。
protect();
// 返回对象在页面中的地址,用于验证。
return ret;
}
总结一下:
对象调用autorelease,底层进入autorelease函数,过滤TaggedPointer后调用autoreleaseFast。
autoreleaseFast首先获取当前线程hotPage热页:
- 热页存在且未满,调用
AutoreleasePoolPage::add直接写入页面。add 会先解除页面保护,写入对象指针、移动 next 写入指针,随后重新保护页面 - 热页存在但是已满,调用autoreleaseFullPage新建页面接入双向链表。
- 线程无页面时执行autoreleaseNoPage,初始化第一个页面。
AutoreleasePoolPage 是自动释放池底层实现:自动释放池是一个页的结构,多个页面构成双向链表,每个线程独立持有一条链表。页面存储autorelease对象指针与pool_boundary边界哨兵。当自动释放池drain时,向前遍历页面内对象执行release,直到碰到边界哨兵停止。
运行期和编译期
Q:ARC在编译期和运行期做了什么?
A:
- 编译期:
- 根据OC内存命名规范分析对象所有权。
- 在合适位置自动插入retain、release、autorelease等Runtime调用。
- 静态优化,剔除互相抵消的冗余内存操作。
- 检查所有权修饰符语法,禁止MRC API。
- 运行期:
- 支持返回值优化,消除autorelease+retain冗余操作。
- 管理__weak弱引用,对象销毁时自动将弱指针置nil。
- 通过AutoreleasePoolPage完成自动释放池对象管理。
- 动态维护引用计数,执行对象销毁逻辑。