【iOS】——ARC源码探究

一、ARC介绍

ARC的全称Auto Reference Counting. 也就是自动引用计数。使用MRC时开发者不得不花大量的时间在内存管理上,并且容易出现内存泄漏或者release一个已被释放的对象,导致crash。后来,Apple引入了ARC。使用ARC,开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码,再结合Objective-C的runtime,实现自动引用计数。

在Objective C中,有三种类型是ARC适用的:

  • block
  • OC的对象,id, Class, NSError*等
  • 由__attribute__((NSObject))标记的类型。

像double *,CFStringRef等不是ARC适用的,仍然需要手动管理内存。

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

1、编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

2、相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

ARC 也附带有运行期的一些机制来使 ARC 能够更好的工作,主要是指 weak 关键字。weak 变量能够在引用计数为 0 时被自动设置成 nil,显然是有运行时逻辑在工作的。

ARC背后的引用计数主要依赖于这三个方法:

retain 增加引用计数
release 降低引用计数,引用计数为0的时候,释放对象。
autorelease 在当前的auto release pool结束后,降低引用计数。

二、源码探究

retain函数

objective-c 复制代码
inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

首先进行通过assert函数断言检查确保当前对象不是taggedPointer。因为标记指针的内存管理方式与普通对象不同。

接着检查对象的类是否具有定制的retain/release行为。如果没有,那么调用rootRetain()函数,这是一个更快的内部函数,直接更新引用计数,避免了消息发送的开销。

如果类有定制的retain/release行为,那么使用objc_msgSend发送retain消息到对象。

taggedPointer即标签指针,如果存储的是某些比较小的对象,相比于以往指向在堆上的对象的指针,taggedPointer可以直接在指针中编码数据,从而提高性能。

接着来看rootRetain函数

rootRetain()函数

objective-c 复制代码
ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false); //传递false和false参数。
}

这里会调用重载的rootRetain函数并传入两个false参数

objective-c 复制代码
ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    return rootRetain(true, false) ? true : false; // 调用rootRetain,参数为true和false。
}

这里是尝试性调用重载的rootRetain()并传入ture和false两个参数,返回true如果成功增加引用计数,否则返回false。

objective-c 复制代码
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    // Inline函数,用于核心的retain操作,接受两个bool参数:
    // tryRetain决定是否为尝试性retain;
    // handleOverflow决定是否处理引用计数溢出。

    if (isTaggedPointer()) return (id)this; // 如果当前对象是标记指针,直接返回this,因为标记指针有自己的内存管理机制。

    bool sideTableLocked = false; // 初始化侧边表锁定状态为未锁定。
    bool transcribeToSideTable = false; // 初始化是否需要将数据转录到侧边表为否。

    isa_t oldisa; // 定义oldisa变量,用于存储对象的原始ISA信息。
    isa_t newisa; // 定义newisa变量,用于存储新的ISA信息。

    do {
        transcribeToSideTable = false; // 每次循环前,重置是否需要转录到侧边表的状态。

        oldisa = LoadExclusive(&isa.bits); // 使用LoadExclusive获取ISA的bits字段,保证原子操作。
        // LoadExclusive允许我们独占地读取内存,这样在我们读取之后和写入之前,其他线程不能修改这个内存位置。

        newisa = oldisa; // 复制旧的ISA信息到新ISA中,准备修改。

        if (slowpath(!newisa.nonpointer)) { // 检查ISA的nonpointer标志,判断是否有侧边表。
            // slowpath宏用于指示编译器此条件在正常情况下很少为真,用于优化。
            ClearExclusive(&isa.bits); // 清除独占状态,因为接下来要处理侧边表。
            if (!tryRetain && sideTableLocked) sidetable_unlock(); // 如果不是尝试性retain且侧边表已锁定,解锁侧边表。
            if (tryRetain)                  // 尝试性retain操作,调用侧边表的尝试性retain方法。
                return sidetable_tryRetain() ? (id)this : nil;
            else                            // 非尝试性retain操作,调用侧边表的常规retain方法。
                return sidetable_retain();
        }
        // 以上处理有侧边表的情况,接下来处理没有侧边表的情况。

        // 不检查newisa.fast_rr,因为我们已经调用了任何RR覆盖。
        // fast_rr是用于快速引用计数的字段,当有侧边表时,fast_rr字段可能无效。

        if (slowpath(tryRetain && newisa.deallocating)) { // 如果是尝试性retain且对象正在dealloc中。
            ClearExclusive(&isa.bits); // 清除独占状态,因为对象正在dealloc中。
            if (!tryRetain && sideTableLocked) sidetable_unlock(); // 如果不是尝试性retain且侧边表已锁定,解锁侧边表。
            return nil; // 返回nil,因为尝试性retain失败。
        }
        // 上面的条件判断确保我们不会在对象dealloc时尝试增加引用计数。

        uintptr_t carry; // 定义carry变量,用于保存进位信息。

        // 使用addc函数原子地增加引用计数,同时检查是否溢出。
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);

        if (slowpath(carry)) { // 如果addc操作产生了进位,即发生了溢出。
            if (!handleOverflow) { // 如果不处理溢出。
                ClearExclusive(&isa.bits); // 清除独占状态。
                // 下面调用rootRetain_overflow函数处理溢出情况,参数为tryRetain。
                return rootRetain_overflow(tryRetain);
            }
            // 如果需要处理溢出,准备将一半的引用计数转移到侧边表。
            if (!tryRetain && !sideTableLocked) sidetable_lock(); // 如果不是尝试性retain且侧边表未锁定,锁定侧边表。
            sideTableLocked = true; // 设置侧边表锁定状态为已锁定。
            transcribeToSideTable = true; // 设置需要转录到侧边表的状态为是。
            newisa.extra_rc = RC_HALF; // 设置extra_rc字段为RC_HALF,表示一半的引用计数。
            newisa.has_sidetable_rc = true; // 设置has_sidetable_rc标志,表示侧边表中有引用计数。
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    // StoreExclusive尝试原子地更新ISA的bits字段,如果更新失败(其他线程修改了bits),则继续循环。

    if (slowpath(transcribeToSideTable)) { // 如果需要将数据转录到侧边表。
        // 转录额外的一半引用计数到侧边表,无需锁定。
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); // 如果不是尝试性retain且侧边表已锁定,解锁侧边表。
    // 此处的sidetable_unlock调用确保我们释放了侧边表的锁,除非正在进行尝试性retain。
    
    return (id)this; // 成功增加引用计数后,返回this指针。
}

这段代码很长大致可分为四个部分:对象是否是标记指针、对象是否有侧边表、引用计数是否会溢出、是否是尝试性retain

独占访问是指在某一时刻只有一个线程能够访问特定的资源或内存位置。这是为了避免并发访问导致的数据竞争(和不一致状态。在并发编程中,独占访问通常通过锁或原子操作来实现。

LoadExclusive函数和ClearExclusive函数就是通过原子操作来进行与独占访问有关的操作

当对象的引用计数超过了一定的阈值,通常是因为被多个引用持有,runtime会将一部分引用计数信息移动到侧边表中,以避免在对象的isa字段中存储过大的数值,从而节省空间和提高效率。

objective-c 复制代码
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
	 //省略其他实现...
};

这个数据结构就是存储了一个自旋锁,一个引用计数map。

在实现上,这个引用计数的map以对象的地址作为key,引用计数作为value。这意味着每个对象在侧边表中最多只有一个条目,对象的地址提供了唯一性,便于查找和更新。

release函数

objective-c 复制代码
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

首先进行通过assert函数断言检查确保当前对象不是taggedPointer。

接着检查对象的类是否具有定制的retain/release行为。如果没有,那么调用rootRelease()函数。

如果类有定制的retain/release行为,那么使用objc_msgSend发送release消息到对象。

rootRelease函数

objective-c 复制代码
ALWAYS_INLINE bool
objc_object::rootRelease()
{
    return rootRelease(true, false); // 调用rootRelease的重载版本,参数为true和false。
}
objective-c 复制代码
ALWAYS_INLINE bool
objc_object::rootReleaseShouldDealloc()
{
    return rootRelease(false, false); // 调用rootRelease的重载版本,参数为false和false。
}
objective-c 复制代码
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,表示对象应该被析构。
}

这段代码大致可分为四个部分:对象是否是标记指针、对象是否有侧边表、引用计数是否会下溢、通过侧边表或析构处理下溢。

当引用计数降到0时,对象会被标记为正在析构中,并最终调用dealloc方法进行析构。如果对象有侧边表,它会尝试从侧边表借用引用计数以避免下溢。如果侧边表为空或对象已经在析构中,它会处理overrelease错误或执行析构操作。

ARC规则下autorelease

autorelease函数的作用是把对象放到autorelease pool中,到pool drain的时候,会释放池中的对象。

在ARC规则下,alloc/init/new/copy/mutableCopy开头的方法返回的对象不是autorelease对象

新建一个自定义类

objective-c 复制代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CustomObject : NSObject
+ (instancetype)object;
- (void)dealloc;
@end

NS_ASSUME_NONNULL_END
#import "CustomObject.h"

@implementation CustomObject
//这个方法返回autorelease对象
+ (instancetype)object{
    return [[CustomObject alloc] init];
}

- (void)dealloc{
    NSLog(@"CustomObject Dealloc");
}
@end

下面确定object方法返回的对象是不是autorelease的

objective-c 复制代码
#import <Foundation/Foundation.h>
#import "CustomObject.h"
int main(int argc, const char * argv[]) {
       __weak CustomObject * weakRef;
    {
        CustomObject * temp = [CustomObject object];
        weakRef = temp;
    }
    NSLog(@"%@",weakRef);
}

这是因为[CustomObject object]返回的不是一个autorelease对象,在作用域(大括号)结束后,并不会立刻被释放,所以在NSLog处还能看到对象的地址。

如果把[CustomObject object]替换成[[CustomObject alloc] init],会发现作用域结束后立刻释放。

假如我们用autorelease包裹后:

objective-c 复制代码
    __weak CustomObject * weakRef;
    @autoreleasepool {
        CustomObject * temp = [CustomObject object];
        weakRef = temp;
    }
    NSLog(@"%@",weakRef);

会看到dealloc方法先调用。

放到自动释放池的对象是在超出自动释放池作用域后立即释放的。事实上在iOS 程序启动之后,主线程会启动一个Runloop,这个Runloop在每一次循环是被自动释放池包裹的,在合适的时候对池子进行清空。

对于Cocoa框架来说,提供了两种方式来把对象显式的放入AutoReleasePool.

  • NSAutoreleasePool(只能在MRC下使用)
  • @autoreleasepool {}代码块(ARC和MRC下均可以使用)

下面从源码入手分析autuorelease

autorelease函数

objective-c 复制代码
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);
}

首先进行通过assert函数断言检查确保当前对象不是taggedPointer。

接着检查对象的类是否具有定制的retain/release行为。如果没有,那么调用rootAutorelease()函数。

如果类有定制的retain/release行为,那么使用objc_msgSend发送autorelease()消息到对象。

objective-c 复制代码
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

首先,函数检查对象是否是taggedPointer。如果是taggedPointer,那么 autorelease 操作不需要做任何事情,因为taggedPointer的对象通常较小且生命周期管理是内置的。在这种情况下,函数简单地返回 this 指针。

接下来,函数调用 prepareOptimizedReturn(ReturnAtPlus1) 函数。这个函数检查是否可以在当前调用栈深度 + 1 的地方直接返回,从而跳过自动释放池的操作。这通常在某些优化场景下发生,比如当对象在当前作用域结束时就会被销毁,那么就没有必要将其放入自动释放池中等待稍后的释放。如果可以优化,函数再次直接返回 this 指针。

如果上述两种情况都无法应用,那么函数会调用 rootAutorelease2() 函数。rootAutorelease2() 是 autorelease 操作的核心实现,负责将对象放入当前线程的自动释放池中,以便在适当的时候(通常是作用域结束时)释放对象。

rootAutorelease2函数

objective-c 复制代码
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

首先判断对象是否为taggedPointer,如果不是就调用AutoreleasePoolPage::autorelease这个方法把该对象作为参数。

AutoreleasePoolPage::autorelease函数

objective-c 复制代码
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;
}

// 快速自动释放实现,尝试将对象加入到当前线程的热自动释放池页面。
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);
    }
}

// 自动释放池页面的add方法,用于向页面中添加一个对象。
id *AutoreleasePoolPage::add(id obj)
{
    // 确保页面没有满。
    assert(!full());
    
    // 临时解除保护,允许修改页面。
    unprotect();
    
    // 记录下一个要写入的位置,这里使用next指针。
    // 注意返回的是next-1的地址,但通过直接返回next避免了指针偏移。
    id *ret = next;
    
    // 将对象写入到next指向的位置,然后递增next。
    *next++ = obj;
    
    // 重新保护页面,防止其他线程修改。
    protect();
    
    // 返回对象在页面中的地址,用于验证。
    return ret;
}

autoreleaseFast函数尝试将对象加入到当前线程的"热"自动释放池页面。如果页面存在并且没有满,对象将被直接加入到页面中。如果页面已满,或者页面不存在,将分别调用autoreleaseFullPageautoreleaseNoPage函数分别进行满页和无页的处理。

AutoreleasePoolPage::add方法负责将对象实际添加到页面中。它先解除页面保护,将对象写入到指定位置,然后递增页面的写入指针,最后重新保护页面。这里的保护和解除保护操作是为了确保在多线程环境下页面数据的一致性和安全性。

autorelease方法会把对象存储到AutoreleasePoolPage的双向链表里。等到autorelease pool被drain的时候,把链表内存储的对象删除。所以AutoreleasePoolPage就是自动释放池的内部实现。

一个 poolPage 的大小 是 4096 字节。其中56 bit 用来存储其成员变量,剩下的存储加入到自动释放池中的对象。原因在于在现代操作系统中,内存通常以页面为单位进行分配和管理,而页面的大小通常为4096字节。因此,将自动释放池页面的大小设定为4096字节可以很好地与操作系统内存管理机制对齐

相关推荐
fzxwl25 分钟前
隆重推荐(Android 和 iOS)UI 自动化工具—Maestro
android·ui·ios
运维-大白同学13 小时前
go-中间件的使用
中间件·golang·xcode
若水无华2 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T4 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa