当前位置:网站首页>ARC在编译和运行做了什么?
ARC在编译和运行做了什么?
2022-07-31 09:01:00 【chabuduoxs】
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_initWeak
和objc_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的知识,后面会专门更新一个关于它的文章。
边栏推荐
- 高并发高可用高性能的解决方案
- (C语言基础)原样输入输出
- @RequestBody和@RequestParam区别
- MySQL中InnoDB的多版本并发控制(MVCC)的实现
- [Mini Program Project Development--Jingdong Mall] Custom Search Component of uni-app (Part 1)--Component UI
- 科目三:前方路口直行
- Job hunting product manager [9] How to write a good resume in job hunting season?
- Linux安装mysql
- 哆啦a梦教你页面的转发与重定向
- SSM框架讲解(史上最详细的文章)
猜你喜欢
随机推荐
[MySQL exercises] Chapter 5 · SQL single table query
Golang-based swagger super intimate and super detailed usage guide [there are many pits]
生成随机数
qt在不同的线程中传递自定义结构体参数
MUI获取相机权限
【小程序项目开发--京东商城】uni-app之自定义搜索组件(上)-- 组件UI
The torch distributed training
vue element form表单规则校验 点击提交后直接报数据库错误,没有显示错误信息
matlab常用符号用法总结
Vulkan与OpenGL对比——Vulkan的全新渲染架构
Pytorch学习记录(七):自定义模型 & Auto-Encoders
安装gnome-screenshot截图工具
JSP config对象的简介说明
【小程序项目开发 -- 京东商城】uni-app 商品分类页面(下)
skynet中一条消息从取出到处理完整流程(源码刨析)
Cloud server deployment web project
[Cloud native and 5G] Microservices support 5G core network
刷题《剑指Offer》day06
spark过滤器
How to upgrade nodejs version