【iOS】MRC和ARC

文章目录

MRC

手动引用计数管理,通过调用retain,release和autorelease等的方法来控制对象生命周期

对象操作和方法:

  1. 生成并持有对象:alloc/new/copy/mutableCopy等方法
  2. 持有对象:retain方法
  3. 释放对象:release
  4. 废弃对象:dealloc

对于单个对象的内存管理:

  • 自己生成的对象,自己持有

  • 非自己生成的对象,自己也可以持有

    objc 复制代码
    NSMutableArray* ary = [NSMutableArray array];
    [ary retain];
  • 不再需要自己持有的对象时释放

  • 非自己持有的对象无法释放

多个对象的内存管理

  • 一个对象不持有另一个对象:此时两个实例对象没有关联可以各自释放

  • 一个对象持有另一个对象:持有的对象要在set方法中引用计数加一,释放时候也要把持有的对象销毁

    objc 复制代码
    - (void)setMyObject:(MyObject*)myObj { 
        // 引用计数器 +1
        [myObj retain];
        _myObj = myObj;
    }
    
    - (void)dealloc {
        // 人释放了, 那么房间也需要释放
        [_myObj release];
        NSLog(@"%s", __func__);
     
        [super dealloc];
    }
  • 一个对象持有并且释放掉一个对象后持有另一个对象:此时要在更换原持有前释放第一个对象

    objc 复制代码
    - (void)setMyObject:(MyObject*)myObj { 
    		[_myObj release];
        // 引用计数器 +1
        [myObj retain];
        _myObj = myObj;
    }
  • 一个对象持有一个对象且释放该对象后再次持有该对象:将那个对象赋值给该对象后该对象对那个对象的引用计数加一,再将那个对象引用计数减一,此时两个对象的引用计数相同,此时再次进行赋值会导致那个对象的引用计数先减一此时为0变成了野指针,此时再对野指针进行retain操作就会报错

    此时有两种方法,一种是先retain再release,还有一种实际应用方法就是先在setter方法中判断是否重复赋值,如果是同一个实例对象,就不需要重复进行release和retain

    objective-c 复制代码
    - (void)setObj:(NSObject *)obj {
        [obj retain];            //先持有新的
        [_obj release];          //再释放旧的
        _obj = obj;              //赋值
    }
    
    - (void)setMyObject:(MyObject*)myObj { 
       if (_myObj != myObj) {//先判断
       [_myObj release];
        // 引用计数器 +1
        [myObj retain];
        _myObj = myObj;
       }
    }

@property参数

  • 在成员变量前加@property,系统会帮我们自动生成setter/getter方法,但是不会生成内存管理相关代码(默认后面什么都不写就是assign)

    objc 复制代码
    @property (nonatomic) int val;
  • 如果在 property 后边加上 retain,系统就会自动帮我们生成 getter / setter 方法内存管理的代码,但是仍需要我们自己重写 dealloc 方法

    objc 复制代码
    @property(nonatomic, retain)MyObject *myObj;

自动释放池

autorelease 是一种支持引用计数的内存管理方式,主要用于延迟对象的释放,只要给对象发送一条 autorelease 消息,会将对象注册到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次 release 操作。

autorelease 方法会返回对象本身,且调用完autorelease方法之后,对象的计数器不变

创建

  • 第一种:使用NSAutoreleasePool创建

    objc 复制代码
    NSAutoreleasePoo *pool = [[NSAutoreleasePool alloc] init]; //创建自动释放池
    [pool release];  
    [pool drain]; //销毁自动释放池
  • 第二种:使用@autoreleasepool创建

    objc 复制代码
    @autoreleasepool
    { // 开始代表创建自动释放池
     
    } // 结束代表销毁自动释放池

使用

对象调用autorelease方法要在创建autoreleasepool之后否则他无法将其注册到autoreleasepool

objc 复制代码
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];

@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
        // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)

//以上两种使用方法相同

自动释放池的嵌套

  • 自动释放池是以栈的形式存在的
  • 所以调用autorelease会把对象放到栈顶的自动释放池
objc 复制代码
@autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
        @autoreleasepool { // 栈顶自动释放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] autorelease];
    }
}

注意事项

  • 自动释放池中不易放占用内存比较大的对象:因为自动释放池时延迟释放机制,只有清空池子时才会把注册到池子的对象销毁,此时如果有多个内存较大的对象就会造成短时间内的内存暴涨

  • 不把大量循环操作放到同一个@autoreleasepool之间,这样会造成内存峰值的上升,若一定要循环,则:

    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,也不调用后又调用release

    objc 复制代码
    @autoreleassepool {
     // 会导致过度释放
        Person *p = [[[[Person alloc] init] autorelease] autorelease];
    }
    
    @autoreleasepool {
        Person *p = [[[Person alloc] init] autorelease];
        [p release]; //过度释放
    }

循环引用

两个对象互相持有对方,A对象的销毁依赖于B,B对象的销毁依赖于A

  1. 自循环引用:若有一个对象,内部强持有他的成员变量obj,若此时我们给obj赋值为原对象时,就是自循环引用
  2. 相互循环引用:对象A内部强持有obj,对象B内部强持有obj,若此时对象A的obj指向对象A,就是相互引用
  3. 多循环引用:假如类中有对象1...对象N,每个对象中都强持有一个obj,若每个对象obj都指向下个对象,就产生了多循环引用

ARC

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

ARC管理的三种类型:

  • OC对象类型id等
  • Block
  • 由 __ attribute __ ((NSObject))标记的类型(把c类型伪装成OC对象)

ARC只管理"对象指针",不管理如int,struct等的普通C类型(CF对象也默认不归ARC管)

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

其主要依赖这三个方法:

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

retain函数

c++ 复制代码
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函数

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

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

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

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

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

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

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

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

LoadExclusive函数(开始独占访问)和ClearExclusive函数就是通过原子操作来进行与独占访问有关的操作。当对象的引用计数超过了一定的阈值,通常是因为被多个引用持有,runtime会将一部分引用计数信息移动到侧边表中,以避免在对象的isa字段中存储过大的数值,从而节省空间和提高效率。

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函数

c++ 复制代码
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函数

c++ 复制代码
ALWAYS_INLINE bool
objc_object::rootRelease()
{
    return rootRelease(true, false); // 调用rootRelease的重载版本,参数为true和false。
}
c++ 复制代码
ALWAYS_INLINE bool
objc_object::rootReleaseShouldDealloc()
{
    return rootRelease(false, false); // 调用rootRelease的重载版本,参数为false和false。
}
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

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

objc 复制代码
#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函数

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);
}

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

c++ 复制代码
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

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

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

AutoreleasePoolPage::autorelease函数

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

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

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

对于自动释放池的总结:

1、自动释放池一个关于指针的栈

2、其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界)

3、自动释放池是一个页的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中提及过,即类的继承链)

4、自动释放池和线程有关系

c++ 复制代码
@autoreleasepool { // 外层池
    id a = ...; // A
    id b = ...; // B
    @autoreleasepool { // 内层池
        id c = ...; // C
        id d = ...; // D
    } // 内层池销毁
    id e = ...; // E
    id f = ...; // F
} // 外层池销毁

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

ARC本质上是一套由编译期和Runtime协同实现的自动内存管理机制

在编译期:Clang 根据所有权规则自动插入 retain、release、autorelease 代码,同时把能互相抵消的操作优化掉,开发者不用手动管理。

在运行期:Runtime 主要负责三件事:第一,真正执行引用计数操作;第二,维护 weak 弱引用表,对象释放时自动置 nil;第三,管理 AutoreleasePool 的 push/pop,并通过返回值优化减少多余的 autorelease 和 retain

  1. 返回值优化具体怎么做的:objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue配合,通过全局标志位跳过不必要的autorelease
  2. weak怎么做到自动置nil:dealloc时走weak_clear_no_lock,遍历weak表把指向该对象的所有weak指针置nil
  3. 编译期优化能举个例子吗:连续retain两次中间无其他操作,编译器会合并成一次
相关推荐
ZZH_AI项目交付2 小时前
同一套前摄心率算法,为什么 iPhone 16 Pro 稳,iPhone XR 会失准?
ios·app·ai编程
DeMinds3 小时前
内容没有丢,我为什么总在重新整理?|DeMinds 如何让工作接着继续
ios·github·markdown
apd_csdn3 小时前
清华邮箱苹果邮件app设置(全)
ios·thu
寒水馨3 小时前
macOS下载、安装openclaw-v2026.7.1(附安装包OpenClaw-2026.7.1.dmg)
macos·大模型·github·开源软件·ai助手·openclaw·gpt-5.6
MDM.Plus4 小时前
从“遥控”到“自治”:苹果 MDM 技术的代际跨越与业务重构
ios·智能手机·重构·mdm
黑化旺仔4 小时前
iOS - 天气预报仿写总结
ios
Coffeeee5 小时前
ios零基础的Android开发能否靠AI让老板省一笔人工费呢
android·ios·ai编程
2501_916007475 小时前
深入理解HTTPS对称与非对称加密机制及Charles抓包实践
网络协议·http·ios·小程序·https·uni-app·iphone
2501_916008895 小时前
HTTPS 抓包遇到证书绑定怎么办,使用 TraceEagle 解除 App 证书校验
网络协议·计算机网络·http·网络安全·ios·adb·https