「iOS」————ARC

iOS学习

    • ARC
        • retain函数
        • **release函数**
        • ARC规则下的autorelease
          • autorelease函数
    • ARC在编译期和运行期做了什么


ARC

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

三种类型是ARC使用的:

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

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

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

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

retain函数
inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

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

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

首先断言检查是否为标签指针,不是的话判断是否具有定制的retain/release行为。如果没有,那么调用rootRetain()函数。这是一个更快的内部函数,直接更新引用计数,避免了消息发送的开销。

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

rootRetain()函数

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false); //传递false和false参数。
}

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

C++ 中的函数重载(Function Overloading) 机制,指的是同名函数通过参数列表的不同(参数类型或数量)来区分不同的实现

ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    return rootRetain(true, false) ? true : false; // 调用rootRetain,参数为true和false。
}

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

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

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

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

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

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

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函数
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

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

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

与retain函数的逻辑几乎一致,是否实现定制的retain/release

rootRelease函数

ALWAYS_INLINE bool
objc_object::rootRelease()
{
    return rootRelease(true, false); // 调用rootRelease的重载版本,参数为true和false。
}
ALWAYS_INLINE bool
objc_object::rootReleaseShouldDealloc()
{
    return rootRelease(false, false); // 调用rootRelease的重载版本,参数为false和false。
}

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

在ARC规则下,alloc/init/new/copy/mutableCopy开头的方法返回的对象不是autorelease对象。我们可以改写delloc方法,判断作用域结束后,对象会不会被立刻释放,来判断是否为autorelease对象。

#import <Foundation/Foundation.h>
#import "CustomObject.h"
int main(int argc, const char * argv[]) {
       __weak CustomObject * weakRef;
    {
        CustomObject * temp = [CustomObject object];//此处不是autorelease对象,改为alloc init则是。主要判断在于weakRef是否立刻销毁。
        weakRef = temp;
    }
    NSLog(@"%@",weakRef);
}

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

  • NSAutoreleasePool(只能在MRC下使用)
  • @autoreleasepool {}代码块(ARC和MRC下均可以使用)
autorelease函数
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);
}

先断言,再判断是否有定制的retain/release行为

inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

先判断是否为标签指针,是的话返回this指针即可。标签指针内置生命管理周期。

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

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

rootAutorelease2

__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

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

AutoreleasePoolPage::autorelease函数

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函数尝试将对象加入到当前线程的“热”自动释放池页面。如果页面存在并且没有满,对象将被直接加入到页面中。如果页面已满,或者页面不存在,将分别调用autoreleaseFullPage和autoreleaseNoPage函数分别进行满页和无页的处理。

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

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

对于自动释放池的总结:

  • 1、自动释放池一个关于指针的栈
  • 2、其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界
  • 3、自动释放池是一个的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中提及过,即类的继承链)
  • 4、自动释放池和线程有关系
@autoreleasepool { // 外层池
    id a = ...; // A
    id b = ...; // B
    @autoreleasepool { // 内层池
        id c = ...; // C
        id d = ...; // D
    } // 内层池销毁
    id e = ...; // E
    id f = ...; // F
} // 外层池销毁

ARC在编译期和运行期做了什么

编译期和运行期ARC做的事情:

  • 在编译期,ARC会把互相抵消的retain、release、autorelease操作约简。并且自动插入引用计数操作
  • ARC包含有运行期组件,可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。
    • 处理弱引用(运行期置nil)
    • 自动释放池(运行期加入池和发送release,实际管理运行期)
    • 动态行为管理

你可能感兴趣的:(ios,cocoa,macos,objective-c)