【iOS】MRC&ARC

【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:

  • 编译期:
  1. 根据OC内存命名规范分析对象所有权。
  2. 在合适位置自动插入retain、release、autorelease等Runtime调用。
  3. 静态优化,剔除互相抵消的冗余内存操作。
  4. 检查所有权修饰符语法,禁止MRC API。
  • 运行期:
  1. 支持返回值优化,消除autorelease+retain冗余操作。
  2. 管理__weak弱引用,对象销毁时自动将弱指针置nil。
  3. 通过AutoreleasePoolPage完成自动释放池对象管理。
  4. 动态维护引用计数,执行对象销毁逻辑。
相关推荐
_瑞3 小时前
怎么知道 App 到底卡在哪?
ios·xcode
Eric Woo X3 小时前
macOS 旧版本无法启动 Steam ?一条命令恢复运行(2026年7月实测)
macos·策略模式
MindUp3 小时前
iPhone 存储空间不足的几种技术性解决方案(云存储同步与外接设备对比)
ios·iphone
游戏开发爱好者83 小时前
iOS应用加固方案全解析:源码加固与IPA在线加固对比
android·macos·ios·小程序·uni-app·cocoa·iphone
yichudu4 小时前
hp 打印机在 mac 下的使用
macos
2501_9159214319 小时前
iOS开发环境搭建详解 Xcode 配置与快蝎轻量级工具选择
ide·vscode·macos·ios·个人开发·xcode·敏捷流程
_瑞21 小时前
试图教会你用 Xcode Instruments
前端·ios·xcode
JoyCong19981 天前
为什么连接到被控后,客户端会自动锁定+出现锁屏界面?
macos·智能手机·电脑·远程工作·远程操作
芳草萋萋鹦鹉洲哦1 天前
【git】重新生成并添加 SSH Key(Mac)
git·macos·ssh