当前位置: 首页 > ai >正文

【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对象的 retainCount1

    objNSObject对象的唯一强引用(无其他指针指向它),则 retainCount1后变为 0,触发 dealloc方法,堆内存被操作系统回收。

    若存在其他强引用(如全局变量、其他对象的属性),则 retainCount不为 0,对象保留在堆中。

手动引用计数(MRC)解析

  MRC 是 OC 早期的内存管理方式,开发者需手动调用内存管理方法(如 retainreleaseautorelease)来控制对象的生命周期即引用计数。在MRC下,每个对象都有一个与之关联的引用计数器,用于记录该对象的引用次数。当一个对象被创建时,其引用计数器被初始化为1。当一个对象被赋值给另一个对象时,其引用计数器加1。当一个对象的引用不再需要时,其引用计数器减1。当一个对象的引用计数器变为0时,该对象被视为无用,并被释放。
  MRC机制下可能会出现内存泄漏和悬挂指针等问题。所以使用时需要关注内存管理的细节,包括何时释放对象。虽然MRC机制能够提供更好的内存控制,但它也增加了编程的复杂性。

关于内存泄漏和悬挂指针

1.内存泄漏

内存泄漏指程序中动态分配的内存未被正确释放,导致这部分内存无法被操作系统回收,最终造成可用内存逐渐耗尽的现象。

关键特征:内存被「占用但未释放」,程序无法再次使用这部分内存。

2.悬挂指针

悬挂指针指指针变量指向的内存已被释放或无效,但指针本身未被显式置为 nilNULL。此时指针仍保留原内存地址,访问该指针会导致程序崩溃。

关键特征:指针「指向无效内存」,访问时触发未定义行为(如崩溃)。
在这里插入图片描述

说到这里,我们再回顾一下什么是野指针,野指针和空指针有什么区别。

空指针:空指针是指指向无效内存地址的指针,通常用 NULL(C/C++)或 nil(Objective-C)显式表示,一般是我们主动设置的。程序访问空指针不会崩溃。

野指针:野指针是指指向已被释放或无效内存的指针,但指针本身未被显式置为 nilNULL。程序访问野指针会导致崩溃。在MRC机制下,野指针就是导致悬挂指针的罪魁祸首。

eg:

在这里插入图片描述

上述的obj就是野指针,释放后未被显式置为 nilNULL,程序再次对其进行访问时就会崩溃。

请添加图片描述

上述指针就是空指针,被显式置为 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 对象(idClassNSError*等);
    • attribute__((NSObject))标记的 C 结构体/联合体
  • 需手动管理的类型
    • 基本数据类型(doubleint等);
    • Core Foundation 对象(CFStringRefCFArrayRef等);
    • 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开头,则编译器会为它们返回的对象自动插入 retainrelease代码;在MRC中,就需要我们手动保留和释放返回的对象。如果不以上面这些开头,则不用调用者保留和释放,因为方法内部会自动执行autorelease方法。

下面来看一些实例:

实例1:

首先我们创建一个Person类,并写入两个类方法,一个以create开头,一个以new开头:

请添加图片描述

然后我们在主函数中调用这两个函数:
请添加图片描述

打上断点后,我们来看看它的汇编代码:
请添加图片描述

从断点来看,第15~20行是我们调用createPerson方法的汇编语言,21~24行是我们调用newPerson方法的汇编语言。下面我们来具体解析一下:

我们我可看到,调用newPerson方法时,编译器会先调用objc_msgSend$newPerson通过消息派发机制调用newPerson方法,然后有一个objc_release标识位,意味着编译器会立即调用objc_release减少对象引用计数,这就是之前说的==“编译器会为它们返回的对象自动插入 retainrelease代码”==。

而当我们调用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(自动引用计数)下的强引用赋值操作。其核心功能是:

  1. 对目标指针指向的旧对象执行 release(释放旧引用)。
  2. 对新对象执行 retain(增加新引用计数)。
  3. 将新对象赋值给目标指针。

其伪代码逻辑如下:

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)。这样即使后续自动释放池销毁时尝试释放该对象(引用计数减 11),对象也不会被销毁,直到调用者主动释放(引用计数减至 0)。

避免重复释放

若返回的对象未被立即 retain(例如被临时变量持有后丢弃),objc_retainAutoreleasedReturnValue不会执行额外操作,对象会在自动释放池销毁时正常释放。

由此,我们可以知道createPerson方法内部通过 alloc/init创建 Person对象,返回值被标记为 autorelease(引用计数初始为 1),编译器在返回前插入 _objc_autoreleaseReturnValue,检测到返回值会被 strong变量 p接收(即将被 retain),触发 objc_retainAutoreleasedReturnValue,将对象引用计数从 1增加到 2(提前保留),自动释放池销毁时,对象引用计数减 11(不会被销毁),当 p超出作用域被释放时,引用计数减至 0,对象正常销毁。

总结

  理解MRC和ARC的底层原理对我们来说可以更好的理解编译器底层的运行过程,让我们对内存管理有更好的理解。现在xcode使用的ARC很方便,但也不是绝对万能的,有时候还是需要我们来手动管理。

http://www.xdnf.cn/news/15660.html

相关文章:

  • Fish Speech:开源多语言语音合成的革命性突破
  • 伺服电机与步进电机要点详解
  • 专题:2025智能体研究报告|附70份报告PDF、原数据表汇总下载
  • 质变科技亮相可信数据库发展大会,参编《数据库发展研究报告2025》
  • Linux学习之认识Linux的基本指令
  • 前端性能优化“核武器”:新一代图片格式(AVIF/WebP)与自动化优化流程实战
  • 多模态大模型重构人机交互,全感官时代已来
  • 微服务项目总结
  • 短视频矩阵系统:选择与开发的全方位指南
  • Python网络爬虫实现selenium对百度识图二次开发以及批量保存Excel
  • Java学习------使用Jemter测试若依项目自定义的功能
  • Unity 常见数据结构分析与实战展示 C#
  • APIs案例及知识点串讲(下)
  • CES Asia 2025备受瞩目,跨国企业锁定亚洲战略首发契机
  • 基于Ubuntu22.04源码安装配置RabbitVCS过程记录
  • ARM64高速缓存,内存属性及MAIR配置
  • 基于华为openEuler系统安装DailyNotes个人笔记管理工具
  • Java全栈面试实录:从Spring Boot到AI大模型的深度解析
  • Glary Utilities (PC维护百宝箱) v6.24.0.28 便携版
  • 云原生 DevOps 实战之Jenkins+Gitee+Harbor+Kubernetes 构建自动化部署体系
  • 密码学基础概念详解:从古典加密到现代密码体系
  • 外网访问基于 Git 的开源文件管理系统 Gogs
  • Anime.js 超级炫酷的网页动画库之SVG路径动画
  • 信息检索革命:Perplexica+cpolar打造你的专属智能搜索中枢
  • GI6E 加密GRID電碼通信SHELLCODE載入
  • 论文review SfM MVS VGGT: Visual Geometry Grounded Transformer
  • 需要保存至服务器的:常见编辑、发布文章页面基础技巧
  • 配置本地git到gitlab并推送
  • elasticsearch+logstash+kibana+filebeat实现niginx日志收集(未过滤日志内容)
  • .QOI: Lossless Image Compression in O(n) Time