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

ARC?

ARC即自动引用计数。具体介绍以及ARC规则可以看我之前写的另外一篇文章——ARC规则

本篇主要探究ARC在背后究竟做了什么?
先下结论,后续都是围绕这个展开的。
1.在编译期,ARC会把互相抵消的retain、release、autorelease操作约简。
2.ARC包含有运行期组件,可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。

ARC简化引用计数

在Clang编译器项目带有一个静态分析器(static analyzer)用于指名程序里引用计数出问题的地方。
在使用ARC时一定要记住, **引用计数实际上还是要执行的, 只不过保留与释放操作现在是由ARC自动为你添加。**由于ARC会自动执行 retain、release、autorelease、decalloc等操作, 所以直接在ARC下调用这些内存管理方法是非法的。
实际上, ARC在调用这些方法时, 并不通过普通的 Objective-C消息派发机制,而是直接调用其底层C语言版本。这样做性能更好, 因为保留及释放操作需要频繁执行, 所以直接调用底层函数能节省很多CPU周期。

通过底层汇编看看ARC是怎么工作的

首先自定义一个类Test
该类有两个类方法,区别在于一个以new开头,调用者持有返回的对象,而create开头的则不持有。
这个也是对应了ARC的方法命名规则:
将内存管理语义在方法名中表示出来早已成为 Objective-C的惯例, 而ARC则将之确立为硬性规定。若方法名以下列词语开头, 则其返回的对象归调用者所有: alloc、new、copy、mutable Copy。若调用上述开头的方法就要负责释放返回的对象。也就是说, 这些对象在MRC中需要你手动的进行释放。若方法名不以上述四个词语开头, 返回的对象就不需要你手动去释放, 因为在方法内部将会自动执行一次autorelease方法。

//
//  Test.h
//  TestArc
//
//  Created by 差不多先生 on 2022/7/18.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Test : NSObject

+ (instancetype)createTest;
+ (instancetype)newTest;
@end

NS_ASSUME_NONNULL_END
#import "Test.h"

@implementation Test
+ (instancetype)createTest {
    
    
    return [[self alloc] init];
}
+ (instancetype)newTest {
    
    
    return [[self alloc] init];
}
@end

测试的情景:

#import "Test.h"
@interface ViewController ()
@property (nonatomic, strong) Test* test;
@end

@implementation ViewController

- (void)viewDidLoad {
    
    
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    // 自己持有返回对象
    [Test newTest]; // test1
//    id temp = [Test newTest]; // test2
//    self.test = [Test newTest]; // 3
//    // 非自己持有返回对象
    [Test createTest]; // 4
//    id temp2 = [Test createTest]; // 5
//    self.test = [Test createTest]; // 6
}

Test1

现在开始查看底层的汇编语言,可以通过编译器的debug调试。
在这里插入图片描述
可以看见主要出现了20 21 行的两个方法。
objc_msgSend: 这个就是向Test发送消息newTest。
objc_release: release的底层版本,释放newTest返回的对象。

可以看出ARC为我们自动添加了release操作,ARC自动添加代码应该如下:

id temp = [Test newTest];
objc_release(temp) ;

Test2

在这里插入图片描述
可以看到这次出现了一个新的底层函数objc_storeStrong:,相当于ARC在底层添加了
objc_storeStrong(&temp1, nil);看看objc库中其具体实现。

// strong
void
objc_storeStrong(id *location, id obj)
{
    
    
    id prev = *location;
    if (obj == prev) {
    
    
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

上面的代码做了以下四件事情:

  • 检查输入的 obj 地址 和指针指向的地址是否相同。
  • 持有对象,引用计数 + 1 。
  • 指针指向 obj。
  • 原来指向的对象引用计数 - 1。
    在本例子中相当于temp1持有newTest返回的对象,并且引用计数 + 1

Test3

在这里插入图片描述
这里发了两次消息,新增的消息是发送setTest的,ARC在setTest的加入了objc_storeStrong。
所以ARC填入后完整代码是:

id temp = [Test newTest];
    [self setTest:temp];
    objc_release(temp);
 - (void)setTest:(Test *test) {
    
    
    objc_storeStrong(&_test, test);
}

Test4

接下来的4 5 6都采用了createTest,所以不持有返回对象,不需要手动释放。
在这里插入图片描述
这里ARC修改后的代码应该是这样的:

// Test
+ (instancetype)createTest {
    
    
    id temp = [self new];  
    return objc_autoreleaseReturnValue(temp); 
} 
// VC
- (void)testForARC {
    
     
    objc_unsafeClaimAutoreleasedReturnValue([Test createTest]); 
}


这里可以看出不仅仅多了一个objc_unsafeClaimAutoreleasedReturnValue,并且在create中多了objc_autoreleaseReturnValue,这是因为ARC规则,无需手动释放的内部自动autorelease。
objc_autoreleaseReturnValue: 这个函数的作用相当于代替我们手动调用 autorelease, 创建了一个autorelease对象。编译器会检测之后的代码, 根据返回的对象是否执行 retain操作, 来设置全局数据结构中的一个标志位, 来决定是否会执行 autorelease操作。该标记有两个状态, ReturnAtPlus0代表执行 autorelease, 以及ReturnAtPlus1代表不执行 autorelease

objc_unsafeClaimAutoreleasedReturnValue: 这个函数的作用是对autorelease对象不做处理仅仅返回,对非autorelease对象调用objc_release函数并返回。所以本情景中它创建时执行了 autorelease操作了,就不会对其进行 release操作了。只是返回了对象,在合适的实际autoreleasepool会对其进行释放的。

Test5

在这里插入图片描述
objc_retainAutoreleasedReturnValue: 这个函数将替代 MRC中的 retain方法, 此函数也会检测刚才提到的那个标志位, 如果为ReturnAtPlus0怎执行该对象的 retain操作,否则直接返回对象本身。

在这个例子中, 由于代码中没有对对象进行保留, 所以创建时objc_autoreleaseReturnValue函数设置的标志位状态是应该是ReturnAtPlus0。所以, 该函数在此处是会进行 retain操作的。
ARC键入后代码

// Test
+ (instancetype)createTest {
    
    
    id temp = [self new];  
    return objc_autoreleaseReturnValue(temp); 
} 
// VC
- (void)testForARC {
    
     
    id temp2 = objc_retainAutoreleasedReturnValue([Test createTest]); 
    objc_storeStrong(&temp2, nil);
}

Test6和上面类似,不多赘述。

ARC对于修饰符的优化

__strong

和Test2相同

__weak

__weak id temp3 = [Test newTest];

在这里插入图片描述
ARC键入后代码:

- (void)testForARC {
    
    
    id temp = [Test newTest]; 
    objc_initWeak(&temp3, temp);
    objc_release(temp);
    objc_destroyWeak(&temp3);
}


这个过程就是weak指针的一个周期, 从创建到销毁。这上面两个新的runtime函数, objc_initWeakobjc_destroyWeak。这两个函数就是负责创建weak指针和销毁weak指针的。其实, 这两个函数内部都引用另一个runtime函数, storeWeak, 它是和storeStrong对应的一个函数。
他两的源码如下:

objc_initWeak(id *location, id newObj)
{
    
    
    if (!newObj) {
    
    
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

void
objc_destroyWeak(id *location)
{
    
    
    (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
        (location, nil);
}


__unsafe_unretained

这种情况和情景一相同ARC仅仅添加了objc_release.

- (void)testForARC {
    
    
    id temp = [Test newTest]; 
	// 指针objc3赋值过程
	objc_release(temp);
}


__unsafe_unretained类型,不具有所有权,所以只是简单的指针赋值, 没有runtime的函数使用。当临时变量temp销毁后, 指针objc3仍然是指向那块内存, 所以是不安全的。正如其名, unretained, unsafe。

__autoreleasing

在这里插入图片描述
使用__autorelease修饰后, 就相当于为其添加一个autorelease, 当autoreleasepool销毁的时候, 将其释放掉。

ARC不会优化的场景。

ARC适用于绝大多数场景,但并不是万能的,例如performSelect系列有许多方法,带有选择子,编译器不知道选择子具体是什么,必须到了运行期才能确定,因此在编译时,不知道其方法名,没法利用ARC内存规则来判断是否释放。

如果这时我们使用selector(newTest)就会造成内存泄漏,因为ARC此时不会帮助我们添加release。

一些Runtime源码分析

刚刚我们说了retain使引用计数+1,release-1等操作,那么具体内部逻辑是如何实现的呢。

objc_retain

id 
objc_retain(id obj)
{
    
    
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj; // 判断值是否存在指针里面
    return obj->retain();
}

这块简单引一下isa_t,每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。.
在这里插入图片描述
具体runtime底层可以看我之前的文章也有介绍——Runtime 探究元类分类类和对象本质

回顾源码:
此时如果值存在指针里面,直接返回。
接下来系统会自动判断是否支持Nonpointer isa,不支持Nonpointer isa的处理就是直接sidetable_retain

objc_object::rootRetain()
{
    
    
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}
// 说明此时值存在sidetable中 直接对其进行加一操作

支持的话接着往下点击,最终会走到:

ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    
    
    return rootRetain(true, false) ? true : false;
}

// MARK: - rootRetian
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    
    
    // 如果为TaggedPointer 直接返回
    if (isTaggedPointer()) return (id)this;
    // 两种锁默认为false
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
    
    
        transcribeToSideTable = false;
        // 首先通过LoadExclusive()加载旧的oldisa
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        // 如果没有优化
        if (slowpath(!newisa.nonpointer)) {
    
    
            ClearExclusive(&isa.bits);
            // 对锁的一些处理
            
            // 判断是否retain
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // 正在被释放的处理
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(tryRetain && newisa.deallocating)) {
    
    
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        // 溢出标记位置
        uintptr_t carry;
        
        // 如果没有溢出引用计数加1
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        // 判断extra_rc位数能否保存引用计数
        if (slowpath(carry)) {
    
    
            
            // extra_rc溢出; handleOverflow = false
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
    
    
                // 清理isa中数据的原子独占
                ClearExclusive(&isa.bits);
                // 重新调用该函数 再次开始handleOverflow 为 true
                //
                return rootRetain_overflow(tryRetain);
            }
            
            // 有溢出  将extra_rc置为最大值的一半
            // 准备将另外一段复制到sidetable
            // 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;
            // 剩下的给sidetable,设置标记为1 存储
            newisa.has_sidetable_rc = true;
        } // 开启循环 直到存储isa.bits被更新成newisa.bits
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));//存储

    // 有溢出 将另一半的引用计数拷贝到side table里面
    if (slowpath(transcribeToSideTable)) {
    
    
        // Copy the other half of the retain counts to the side table.
        // 存储到side table
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    
    // 返回自身
    return (id)this;
}

理一下上面的流程:

  • TaggedPointer:值存在指针内,直接返回。
  • !newisa.nonpointer:未优化的 isa ,使用sidetable_retain()。
  • newisa.nonpointer:已优化的 isa , 这其中又分 extra_rc 溢出和未溢出的两种情况。
    1.未溢出时,isa.extra_rc + 1 完事。
    2.溢出时,将 isa.extra_rc 中一半值转移至sidetable中,然后将isa.has_sidetable_rc设置为true,表示使用了sidetable来计算引用次数。

如下图所示:
在这里插入图片描述

objc_release

objc_release 和 objc_retain的原理是差不多的。下面看看源码:

void 
objc_release(id obj)
{
    
    
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
ALWAYS_INLINE bool 
objc_object::rootReleaseShouldDealloc()
{
    
    
    return rootRelease(false, false);
}

ALWAYS_INLINE bool 
{
    
    
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
    
    
        
        // 首先加载旧的isa
        oldisa = LoadExclusive(&isa.bits);
        
        // 将旧的isa赋值给newisa
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
    
    
            // 不支持优化操作
            
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            // 入参是否要执行 Dealloc 函数,如果为 true 则执行 SEL_dealloc 否则release
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        // 标记溢出位
        uintptr_t carry;
        // 根据是否下溢以及是否执行dealloc 再次调用
        objc_object::rootRelease(bool performDealloc, bool handleUnderflow)

        // 将extra_rc减1
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        
        // 如果有越界的情况 即extra_rc < 0,走underflow
        if (slowpath(carry)) {
    
    
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));//更新isa的extra_rc

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;
// 跳转到了处理下溢出,从 side table 中借位或者释放
 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;

    // 先判断isa的has_sidetable_rc是否为true
    if (slowpath(newisa.has_sidetable_rc)) {
    
    
        if (!handleUnderflow) {
    
    
            // 清理数据
            ClearExclusive(&isa.bits);
            
            // 调用rootRelease_underflow,处理越界情况
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.
        // 将保留计数从副表side table到内联存储。
        
        if (!sideTableLocked) {
    
    
            // 解除清除原isa中的原子独占
            ClearExclusive(&isa.bits);
            sidetable_lock();
            // 上锁
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            // 跳转到 retry 重新开始,避免 isa 从 nonpointer 类型转换成原始类型导致的问题
            goto retry;
        }

        // 从side table中获取引用计数
        // Try to remove some retain counts from the side table.
        // 默认为一半
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
    
    
            
            // 如果borrowed(side table)的值大于1,将extra_rc 设置为borrowed - 1
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            // 同步修改到isa中
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            // 保存失败
            if (!stored) {
    
    
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                // 重新加载isa
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                // 如果newisa2是nonpointer类型
                if (newisa2.nonpointer) {
    
    
                    uintptr_t overflow;
                      // 将从 SideTables 表中获取的引用计数保存到 newisa2 的 extra_rc 标志位中
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
    
    
                        // 如果没有溢出再次将 isa.bits 中的值更新为 newisa2.bits
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }
            // 再次失败
            if (!stored) {
    
    
                //  // 将从sidetable中取出的引用计数borrowed 重新加到sidetable中
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            //  // 完成对 SideTables 表中数据的操作后,为其解锁
            sidetable_unlock();
            return false;
        }
        else {
    
    
            // 如果side table中值为空,则执行dealloc
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.
    // 如果当前的对象正在被释放
    if (slowpath(newisa.deallocating)) {
    
    
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        // 提示error
        return overrelease_error();
        // does not actually return
    }
    // 将对象被释放的标志位置为true
    newisa.deallocating = true;
    // 将newisa同步到isa中 如果失败 进行重试
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    // 如果需要执行dealloc方法 那么调用该对象的dealloc方法
    if (performDealloc) {
    
    
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

总结一下逻辑:

  • TaggedPointer: 直接返回 false。
  • !nonpointer: 未优化的 isa 执行 sidetable_release。
  • nonpointer:已优化的 isa ,分下溢和未下溢两种情况。
    未下溢: extra_rc–。
    下溢:从 sidetable 中借位给 extra_rc 达到半满,如果无法借位则说明引用计数归零需要进行释放。其中借位时可能保存失败会不断重试。

到这里可以知道 引用计数分别保存在isa.extra_rc和sidetable中,当isa.extra_rc溢出时,将一半计数转移至sidetable中,而当其下溢时,又会将计数转回。当二者都为空时,会执行释放流程 。
在这里插入图片描述

rootRetainCount

objc_object::rootRetainCount方法是用来计算引用计数的。通过前面rootRetain和rootRelease的源码分析可以看出引用计数会分别存在isa.extra_rc和sidetable。中,这一点在rootRetainCount方法中也得到了体现。

// MARK: - 获取对象的引用计数
inline uintptr_t 
objc_object::rootRetainCount()
{
    
    
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
    
    
        
        // 引用计数 = 1 + extra_rc
        uintptr_t rc = 1 + bits.extra_rc;
        
        // 如果side table中有值
        if (bits.has_sidetable_rc) {
    
    
            
            // 再加上side table中的引用计数
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        
        // 策略就是:判断是否是优化型isa
        // 是的话将extra_rc+1
        // 判断SideTable中是否存储引用计数
        // 是的话将extra_rc+1+SideTable中引用计数后返回
        return rc;
    }

    sidetable_unlock();
    // 不是优化型isa指针, 引用计数存储在SideTable中,获取并返回
    return sidetable_retainCount();
}

还有一些autorelease的知识,后面会专门更新一个关于它的文章。

本文参考:理解ARC实现原理
ARC到底帮我们干了什么

猜你喜欢

转载自blog.csdn.net/chabuduoxs/article/details/125887007