【iOS】MRC与ARC
文章目录
- 前言
- 什么是内存管理
- 手动引用计数(MRC)解析
- 关于内存泄漏和悬挂指针
- MRC避免循环引用
- 造成的原因
- 避免的方法
- 自动引用计数(ARC)解析
- ARC的实现
- ARC的底层工作原理
- 总结
前言
众所周知,OC是一种面向对象的编程语言,具有动态类型和垃圾回收的特性。OC为我们提供了两种内存管理机制:手动引用计数(MRC——Manual Reference Counting)和自动引用计数(ARC——Automatic Reference Counting)。接下来,我们分别来了解一下这两种内存管理机制。
什么是内存管理
首先,我们先了解一下什么是内存管理,我们为什么要进行内存管理。程序在运行过程中,通常会通过“创建一个OC对象”、“定义一个变量”、“调用一个函数或方法“等行为,来增加程序的内存占用。而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的,当程序所占内存较多时,系统就会发出警告,这时就需要回收一些目前不使用的内存空间(比如一些不使用的对象、变量等)。如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退等现象,影响用户体验。所以,我们要对内存进行合理分配,及时回收不用的内存,保证程序的稳定。
哪些对象需要我们进行内存管理?
- 任何继承了NSObject的对象需要进行内存管理;
- 其他非对象类型(如int、char、float、double、struct、enum枚举等)不需要进行内存管理。
这是因为
- 继承了NSObject的对象存储在操作系统的堆里面,操作系统的堆一般由程序员分配释放。
- 非OC对象一般放在操作系统的栈里面,操作系统的栈由操作系统自动释放。
eg:
int main(int argc, const char * argv[]) {@autoreleasepool {int a = 10;//栈int b = 20;//栈//obj:栈//NSObject对象(计数器==1):堆NSObject *p = [[NSObject alloc] init];}//经过上面代码后,栈里面的变量a、b、obj都会被回收//但是堆里面的NSObject对象还会留在内存中,因为它的计数器依然是1return 0;
}
栈上的变量行为
- a和b:基本数据类型(
int
),存储在栈中。当@autoreleasepool
块结束时,栈上的变量自动释放,内存归还系统。 - Obj:指针变量(存储
NSObject
对象的内存地址),同样存储在栈中。块结束时,指针变量obj
本身被释放,但它指向的堆内存需由引用计数决定。
堆上 NSObject
对象的创建与释放
-
对象创建:
[[NSObject alloc] init]
调用alloc
方法,向操作系统申请堆内存,并初始化NSObject
对象的isa
指针和引用计数(retainCount = 1
)。 -
引用计数管理(ARC):
ARC 中,局部变量
obj
默认被标记为__strong
(强引用)。当obj
超出作用域(@autoreleasepool
块结束)时,编译器会自动插入[obj release]
,将NSObject
对象的retainCount
减1
。若
obj
是NSObject
对象的唯一强引用(无其他指针指向它),则retainCount
减1
后变为0
,触发dealloc
方法,堆内存被操作系统回收。若存在其他强引用(如全局变量、其他对象的属性),则
retainCount
不为0
,对象保留在堆中。
手动引用计数(MRC)解析
MRC 是 OC 早期的内存管理方式,开发者需手动调用内存管理方法(如 retain
、release
、autorelease
)来控制对象的生命周期即引用计数。在MRC下,每个对象都有一个与之关联的引用计数器,用于记录该对象的引用次数。当一个对象被创建时,其引用计数器被初始化为1。当一个对象被赋值给另一个对象时,其引用计数器加1。当一个对象的引用不再需要时,其引用计数器减1。当一个对象的引用计数器变为0时,该对象被视为无用,并被释放。
MRC机制下可能会出现内存泄漏和悬挂指针等问题。所以使用时需要关注内存管理的细节,包括何时释放对象。虽然MRC机制能够提供更好的内存控制,但它也增加了编程的复杂性。
关于内存泄漏和悬挂指针
1.内存泄漏
内存泄漏指程序中动态分配的内存未被正确释放,导致这部分内存无法被操作系统回收,最终造成可用内存逐渐耗尽的现象。
关键特征:内存被「占用但未释放」,程序无法再次使用这部分内存。
2.悬挂指针
悬挂指针指指针变量指向的内存已被释放或无效,但指针本身未被显式置为 nil
或 NULL
。此时指针仍保留原内存地址,访问该指针会导致程序崩溃。
关键特征:指针「指向无效内存」,访问时触发未定义行为(如崩溃)。
说到这里,我们再回顾一下什么是野指针,野指针和空指针有什么区别。
空指针:空指针是指指向无效内存地址的指针,通常用 NULL
(C/C++)或 nil
(Objective-C)显式表示,一般是我们主动设置的。程序访问空指针不会崩溃。
野指针:野指针是指指向已被释放或无效内存的指针,但指针本身未被显式置为 nil
或 NULL
。程序访问野指针会导致崩溃。在MRC机制下,野指针就是导致悬挂指针的罪魁祸首。
eg:
上述的obj就是野指针,释放后未被显式置为 nil
或 NULL
,程序再次对其进行访问时就会崩溃。
上述指针就是空指针,被显式置为 nil
,虽然已经释放,但因为被置为nil,所以程序再次访问时不会崩溃。
MRC避免循环引用
造成的原因
首先,我们需要明白造成循环引用的本质原因是对象间的强引用闭环。
eg:
// 对象 A 和 B 互相强引用
@interface A : NSObject
@property (nonatomic, retain) B *b; // 强引用 B
@end@interface B : NSObject
@property (nonatomic, retain) A *a; // 强引用 A
@end// 创建对象并形成循环引用
A *a = [[A alloc] init]; // retainCount = 1
B *b = [[B alloc] init]; // retainCount = 1
a.b = b; // b.retainCount = 2(A 强引用)
b.a = a; // a.retainCount = 2(B 强引用)
上述代码中,对象 A 持有对象 B 的强引用,对象 B 持有对象 A 的强引用。当外部无其他引用时,A 和 B 的引用计数均为 1(互相持有),无法降为 0,导致内存泄漏。
避免的方法
1.打破强引用闭环:手动释放其中一个引用
通过手动调用 release
释放其中一个对象的强引用,打破循环。适用于明确知道对象生命周期的场景。我们就以刚刚的A和B为例子:
// 对象 A 和 B 互相强引用
@interface A : NSObject
@property (nonatomic, retain) B *b; // 强引用 B
@end@interface B : NSObject
@property (nonatomic, retain) A *a; // 强引用 A
@end// 创建对象并形成循环引用
A *a = [[A alloc] init]; // retainCount = 1
B *b = [[B alloc] init]; // retainCount = 1
a.b = b; // b.retainCount = 2(A 强引用)
b.a = a; // a.retainCount = 2(B 强引用)// 手动释放其中一个引用(打破循环)
[a release]; // a.retainCount = 1(B 仍强引用)
// 此时,若外部无其他引用,b 的 retainCount 仍为 2(A 已释放,但 B 被 a 强引用?不,a 已释放,b 的 retainCount 应为 1?需要重新梳理)// 正确的手动释放顺序:
// 假设外部不再需要 a 和 b,手动释放 b(原 retainCount 2 → 1)
[b release];
// 再释放 a(原 retainCount 2 → 1)
[a release];
// 此时,a 和 b 的 retainCount 均为 1,但无外部引用,需进一步处理?
// 这种方法风险高,需确保所有引用都被正确释放。
2.使用弱引用(手动实现)
MRC 中虽无原生弱引用,但可通过==不增加引用计数的指针模拟弱引用。例如,使用普通指针(不调用 retain,而是assign)存储对象,避免形成强引用闭环。还是以刚刚的A和B为例:
@interface A : NSObject
@property (nonatomic, assign) B *b; // 弱引用 B(不 retain)
@end@interface B : NSObject
@property (nonatomic, retain) A *a; // 强引用 A
@end// 创建对象
A *a = [[A alloc] init]; // retainCount = 1
B *b = [[B alloc] init]; // retainCount = 1
a.b = b; // B 的 retainCount 仍为 1(A 不 retain B)
b.a = a; // A 的 retainCount = 2(B 强引用)// 外部释放 a 和 b:
[a release]; // A 的 retainCount = 1(B 仍强引用)
[b release]; // B 的 retainCount = 0 → 调用 dealloc,释放其持有的 A(a.retainCount = 0)
// 此时,a 被 B 的 dealloc 释放,无循环引用。
3. 使用中间对象解耦
通过引入一个中间对象(如「协调者」),将原本相互持有的强引用转换为对中间对象的弱引用,打破循环。
// 中间协调者对象
@interface Mediator : NSObject
@property (nonatomic, retain) A *a;
@property (nonatomic, retain) B *b;
@end@interface A : NSObject
@property (nonatomic, assign) Mediator *mediator; // 弱引用 Mediator
@end@interface B : NSObject
@property (nonatomic, assign) Mediator *mediator; // 弱引用 Mediator
@end// 创建对象
Mediator *mediator = [[Mediator alloc] init]; // retainCount = 1
A *a = [[A alloc] init]; // retainCount = 1
B *b = [[B alloc] init]; // retainCount = 1// 对象通过 Mediator 间接关联(弱引用)
a.mediator = mediator;
b.mediator = mediator;
mediator.a = a; // Mediator 强引用 A(retainCount = 2)
mediator.b = b; // Mediator 强引用 B(retainCount = 2)// 释放外部引用:
[a release]; // A.retainCount = 1(Mediator 仍强引用)
[b release]; // B.retainCount = 1(Mediator 仍强引用)
[mediator release]; // Mediator.retainCount = 0 → 调用 dealloc,释放其持有的 A 和 B(A/B retainCount 减 1 → 0)
// 所有对象被正确释放,无循环引用。
4. 使用通知中心(Notification Center)
通过通知中心传递消息,避免对象间直接持有强引用。例如,对象 A 发送通知,对象 B 监听通知并响应,无需持有彼此的引用。
// 对象 A 发送通知
@interface A : NSObject
@end@implementation A
- (void)doSomething {[[NSNotificationCenter defaultCenter] postNotificationName:@"A_DidSomething" object:self];
}
@end// 对象 B 监听通知(不持有 A 的引用)
@interface B : NSObject
@end@implementation B
- (void)handleNotification:(NSNotification *)notification {A *a = notification.object; // 临时获取 A 的引用(不持有)// 处理逻辑...
}
@end// 注册通知监听
B *b = [[B alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:b selector:@selector(handleNotification:) name:@"A_DidSomething" object:nil];// 使用后移除监听(避免野指针)
[b removeObserver:self name:@"A_DidSomething" object:nil];
[b release];
自动引用计数(ARC)解析
自动引用计数是一种更为先进的内存管理机制,它通过编译器自动管理对象的生命周期。在ARC下,编译器会自动插入retain和release语句,以管理对象的引用计数器。当一个对象被创建时,编译器会自动插入retain语句,将其引用计数器设置为1。当一个对象被赋值给另一个对象时,编译器会自动插入retain语句,将其引用计数器加1。当一个对象的引用不再需要时,编译器会自动插入release语句,将其引用计数器减1。当一个对象的引用计数器变为0时,该对象被视为无用,并被释放。
ARC机制下,开发者无需关注内存管理的细节,只需关注代码逻辑即可。这大大简化了编程的复杂性,并减少了内存泄漏和悬挂指针等问题。另外,ARC机制下对象的生命周期更加明确,有助于提高程序的稳定性和可维护性。
- ARC 适用类型:
- Block(Objective-C 对象);
- 所有 Objective-C 对象(
id
、Class
、NSError*
等); - 由
attribute__((NSObject))
标记的 C 结构体/联合体
- 需手动管理的类型:
- 基本数据类型(
double
、int
等); - Core Foundation 对象(
CFStringRef
、CFArrayRef
等); - C 语言手动分配的内存(
malloc
分配的内存)
- 基本数据类型(
笔者之前写过一篇关于ARC的初步认识的文章:【iOS】关于自动引用计数的认识,今天我们主要来探讨一下ARC在编译器和运行期都做了什么?
我们回忆先来一下ARC的基本原理:它是在编译期自动插入内存管理代码,而运行时则负责执行这些代码。
首先,编译器部分。ARC的核心是编译器(Clang)在编译阶段静态分析代码,确定对象的生命周期,插入retain、release、autorelease等方法,处理属性的setter,生成内存管理代码,并处理弱引用(__weak)的存储。
然后是运行时部分。运行时主要负责执行编译器插入的内存管理代码(retain、release),管理引用计数表,跟踪对象的引用计数,处理autorelease池的销毁,触发release调用,并处理弱引用对象的标记和置空。
注意区分编译期和运行期的不同职责:编译期是静态分析,生成必要的代码;运行期是动态执行这些代码,管理对象的生命周期。
注意ARC与MRC的区别:ARC减少了手动操作,但底层仍然依赖引用计数。
ARC的实现
ARC的底层工作原理
根据笔者前面对ARC的简单认识的文章,我们可以知道,ARC有一套命名规则:若方法名以alloc、new、copy、mutableCopy开头,则编译器会为它们返回的对象自动插入 retain
和 release
代码;在MRC中,就需要我们手动保留和释放返回的对象。如果不以上面这些开头,则不用调用者保留和释放,因为方法内部会自动执行autorelease方法。
下面来看一些实例:
实例1:
首先我们创建一个Person类,并写入两个类方法,一个以create开头,一个以new开头:
然后我们在主函数中调用这两个函数:
打上断点后,我们来看看它的汇编代码:
从断点来看,第15~20行是我们调用createPerson方法的汇编语言,21~24行是我们调用newPerson方法的汇编语言。下面我们来具体解析一下:
我们我可看到,调用newPerson方法时,编译器会先调用objc_msgSend$newPerson
通过消息派发机制调用newPerson
方法,然后有一个objc_release标识位,意味着编译器会立即调用objc_release
减少对象引用计数,这就是之前说的==“编译器会为它们返回的对象自动插入 retain
和 release
代码”==。
而当我们调用createPerson方法时,通过会变代码,我们能发现比调用newPerson方法,标识位不是objc_release,而是objc_unsafeClaimAutoreleasedReturnValue。
objc_unsafeClaimAutoreleasedReturnValue方法:
objc_unsafeClaimAutoreleasedReturnValue
是 Objective-C 运行时(ObjC Runtime)中的一个底层函数,主要用于安全接收并传递已被标记为自动释放(autorelease)的对象,避免重复释放或内存泄漏。它是 ARC(自动引用计数)机制中优化内存管理的关键一环,通常与objc_autoreleaseReturnValue
配合使用。核心作用
objc_unsafeClaimAutoreleasedReturnValue
的核心目标是:确保从objc_autoreleaseReturnValue
返回的对象被正确传递,避免因重复释放导致崩溃。具体表现为:
- 若传入的对象是 已被
autorelease
的==(符合预期),则直接返回该对象(由外层autorelease pool
负责释放)。- 若传入的对象是 ==未被
autorelease
的(异常情况),则调用objc_release
手动释放(避免内存泄漏)。
_objc_autoreleasedReturnValue方法:
核心作用
objc_autoreleaseReturnValue
的本质是**“智能自动释放”**,其设计目的是在返回对象时,根据后续代码的使用场景动态决定是否真正执行autorelease
操作,从而减少不必要的内存开销。当一个方法需要返回新创建的对象时,该函数负责处理这个对象的自动释放逻辑。与配对的
_objc_retainAutoreleasedReturnValue
一起工作,形成一个优化机制,可以显著减少 ARC 环境下的内存管理开销。通过预测对象的使用方式,减少不必要的 autorelease 操作。工作原理
当编译器检测到函数/方法需要返回一个对象时,会插入
objc_autoreleaseReturnValue
。此时编译器会预测返回值是否会被调用者立即retain
(例如,调用者可能将返回值作为参数传递给另一个需要强引用的方法):
- 如果预测返回值会被
retain
(如调用者需要长期持有该对象),则不执行autorelease
(标记为ReturnAtPlus1
),避免对象被提前加入autorelease pool
,减少池的负担。- 如果预测返回值不会被
retain
(如调用者仅临时使用),则执行autorelease
(标记为ReturnAtPlus0
),确保对象在autorelease pool
耗尽时被释放,避免内存泄漏。
实例2:
我们在主函数中加入如下代码,打上断点
然后运行后我们能看到汇编程序如下:
我们发现,这里的标识位变成了objc_storeStrong,这又是什么呢?
objc_storeStrong其实是 Objective-C 运行时(Objective-C Runtime)提供的一个内部函数,用于实现 ARC(自动引用计数)下的强引用赋值操作。其核心功能是:
- 对目标指针指向的旧对象执行
release
(释放旧引用)。 - 对新对象执行
retain
(增加新引用计数)。 - 将新对象赋值给目标指针。
其伪代码逻辑如下:
void objc_storeStrong(id *object, id value) {id oldValue = *object;*object = value;[oldValue release]; // 释放旧对象(若存在)[value retain]; // 保留新对象(若存在)
}
总之,objc_storeStrong
函数是用于管理强引用的一个函数,主要用于更新一个指向对象的强引用,并确保正确的内存管理。
同理,使用createPerson方法:
其汇编代码如下:
这个方法比newPerson多了一个objc_retainAutoreleasedReturnValue,这个就是刚刚我们上面了解objc_autoreleaseReturnValue时所提到的OC中相互配对协作的另一个函数了,主要用于优化==自动释放对象(autorelease objects)==的内存管理。
objc_retainAutoreleasedReturnValue
的核心目标是:当自动释放的对象被返回后,若会被立即保留(retain),则提前增加其引用计数,避免被自动释放池错误释放。
在 ARC 环境下,当方法返回一个自动释放的对象(如通过
alloc/init
创建后标记为autorelease
的对象)时,编译器会插入_objc_autoreleaseReturnValue
函数。该函数会根据后续代码对返回值的操作(是否调用retain
),动态决定是否执行自动释放。而objc_retainAutoreleasedReturnValue
则是这一过程的「补充」:场景触发:
当
_objc_autoreleaseReturnValue
检测到返回的对象可能被立即retain
(例如我们通过strong
变量接收返回值),它会通知运行时系统标记该对象为「可能被保留」。提前保留对象:
objc_retainAutoreleasedReturnValue
会在对象被返回后,提前调用retain
增加其引用计数(通常从1
增加到2
)。这样即使后续自动释放池销毁时尝试释放该对象(引用计数减1
至1
),对象也不会被销毁,直到调用者主动释放(引用计数减至0
)。避免重复释放:
若返回的对象未被立即
retain
(例如被临时变量持有后丢弃),objc_retainAutoreleasedReturnValue
不会执行额外操作,对象会在自动释放池销毁时正常释放。
由此,我们可以知道createPerson
方法内部通过 alloc/init
创建 Person
对象,返回值被标记为 autorelease
(引用计数初始为 1
),编译器在返回前插入 _objc_autoreleaseReturnValue
,检测到返回值会被 strong
变量 p
接收(即将被 retain
),触发 objc_retainAutoreleasedReturnValue
,将对象引用计数从 1
增加到 2
(提前保留),自动释放池销毁时,对象引用计数减 1
至 1
(不会被销毁),当 p
超出作用域被释放时,引用计数减至 0
,对象正常销毁。
总结
理解MRC和ARC的底层原理对我们来说可以更好的理解编译器底层的运行过程,让我们对内存管理有更好的理解。现在xcode使用的ARC很方便,但也不是绝对万能的,有时候还是需要我们来手动管理。