iOS ARC 内存管理要点

2024-04-30 17:32
文章标签 内存 管理 ios 要点 arc

本文主要是介绍iOS ARC 内存管理要点,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在讨论 ARC 之前,我们需要知道 Objective-C 采用的是引用计数式的内存管理方式,这一方式的特点是:

  • 自己生成的对象自己持有。比如:NSObject * __strong object = [NSObject alloc] init];
  • 非自己生成的对象自己也能持有。比如:NSMutableArray * __strong array = [NSMutableArray array];
  • 自己持有的对象不再需要时释放。
  • 非自己持有的对象自己无法释放。

而 ARC 则是帮助我们做对象内存管理的一套机制,使得我们以前在 MRC 模式下管理内存工作量能在 ARC 模式下得到缓解。正如苹果官方文档上所描述的:

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects.

可见 ARC 是编译时特性,它没有改变 Objective-C 引用计数式内存管理的本质,更不是 GC(垃圾回收)。

在 ARC 特性下有 4 种与内存管理息息相关的变量所有权修饰符值得我们关注:

  • __strong
  • __weak
  • __autoreleasing
  • __unsafe_unretaied

说到变量所有权修饰符,有人可能会跟属性修饰符搞混,这里做一个对照关系小结:

  • assign 对应的所有权类型是 __unsafe_unretained
  • copy 对应的所有权类型是 __strong
  • retain 对应的所有权类型是 __strong
  • strong 对应的所有权类型是 __strong
  • unsafe_unretained 对应的所有权类型是 __unsafe_unretained
  • weak 对应的所有权类型是 __weak

以上除了 weak 外,其他的属性修饰符在 MRC 模式下也是有效的。

另外,__strong__weak__autoreleasing 修饰的自动变量会自动初始化为 nil。

接下来就一一介绍 4 种变量所有权修饰符。

__strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。__weak 最常见的一个作用就是用来避免强引用循环。但是需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

__weak 的几个使用场景:

  • 在 Delegate 关系中防止强引用循环。在 ARC 特性下,通常我们应该设置 Delegate 属性为weak 的。但是这里有一个疑问,我们常用到的 UITableView 的 delegate 属性是这样定义的: @property (nonatomic, assign) id<UITableViewDelegate> delegate;,为什么用的修饰符是 assign 而不是 weak?其实这个 assign 在 ARC 中意义等同于__unsafe_unretaied(后面会讲到),它是为了在 ARC 特性下兼容 iOS4 及更低版本来实现弱引用机制。一般情况下,你应该尽量使用 weak
  • 在 Block 中防止强引用循环。关于 Block 可以看这篇文章的详细探讨:Block
  • 用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;

在 ARC 模式下,我们不能显示的使用 autorelease 方法了,但是 autorelease 的机制还是有效的,通过将对象赋给 __autoreleasing 修饰的变量就能达到在 MRC 模式下调用对象的 autorelease 方法同样的效果。

__autoreleasing 修饰的对象会被注册到 Autorelease Pool 中,并在 Autorelease Pool 销毁时被释放,和 MRC 特性下的 autorelease 的意义相同。定义 property 时不能使用这个修饰符,因为任何一个对象的 property 都不应该是 autorelease 类型的。

在 ARC 模式下,显式的使用 __autoreleasing 的场景很少见,但是 autorelease 的机制却依然在很多地方默默起着作用。我们来看看这些场景:

  • 方法返回值。
  • 访问 __weak 修饰的变量。
  • id 的指针或对象的指针(id *)。
示例

首先,我们看这个方法:

 
  1. - (NSObject *)object {
  2. NSObject *o = [[NSObject alloc] init];
  3. return o;
  4. }

这里 o 的所有权修饰符是默认的 __strong。由于 return 使得 o 超出其作用域,它强引用持有的对象本该被释放,但是由于该对象作为函数返回值,所以一般情况下编译器会自动将其注册到 Autorelease Pool 中(注意这里是一般情况下,在一些特定情况下,ARC 机制提出了巧妙的运行时优化方案来跳过 autorelease 机制,见后面章节)。这是 autorelease 机制默默起作用的一个例子。

方法返回值时的 autorelease 机制

那么这里有一个问题:为什么方法返回值的时候需要用到 autorelease 机制呢?

这涉及到两个角色的问题。一个角色是调用方法接收返回值的接收方。当参数被作为返回值 return 之后,接收方如果要接着使用它就需要强引用它,使它 retainCount +1,用完后再清理,使它 retainCount -1。有持有就有清理,这是接收方的责任。另一个角色就是返回对象的方法,即提供方。在方法中创建了对象并作为返回值时,一方面你创建了这个对象你就得负责释放它,有创建就有释放,这是创建者的责任。另一方面你得保证返回时对象没被释放以便方法外的接收方能拿到有效的对象,否则你返回的是 nil,有何意义呢。所以就需要找一个合理的机制既能延长这个对象的生命周期,又能保证对其释放。这个机制就是 autorelease 机制

当对象作为参数从方法返回时,会被放到正在使用的 Autorelease Pool 中,由这个 Autorelease Pool 强引用这个对象而不是立即释放,从而延长了对象的生命周期,Autorelease Pool 自己销毁的时候会把它里面的对象都顺手清理掉,从而保证了对象会被释放。但是这里也引出另一个问题:既然会延长对象的生命周期到 Autorelease Pool 被销毁的时候,那么 Autorelease Pool 的生命周期是多久呢?会不会在 Autorelease Pool 都销毁了,接收方还没接收到对象呢?

Autorelease Pool 是与线程一一映射的,这就是说一个 autoreleased 的对象的延迟释放是发生在它所在的 Autorelease Pool 对应的线程上的。因此,在方法返回值的这个场景中,如果 Autorelease Pool 的 drain 方法没有在接收方和提供方交接的过程中触发,那么 autoreleased 对象是不会被释放的(除非严重错乱的使用线程)。

通常,Autorelease Pool 的销毁会被安排在很好的时间点上:

  • Run Loop 会在每次 loop 到尾部时销毁 Autorelease Pool。
  • GCD 的 dispatched blocks 会在一个 Autorelease Pool 的上下文中执行,这个 Autorelease Pool 不时的就被销毁了(依赖于实现细节)。NSOperationQueue 也是类似。
  • 其他线程则会各自对他们对应的 Autorelease Pool 的生命周期负责。

至此,我们知道了为何方法返回值需要 autorelease 机制,以及这一机制是如何保障接收方能从提供方那里获得依然鲜活的对象。

关于 Autorelease Pool 可以看一下这几篇文章:

  • iOS 中的 Autorelease Pool
  • 黑幕背后的Autorelease
  • Objective-C Autorelease Pool 的实现原理
ARC 模式下方法返回值跳过 autorelease 机制的优化方案

在 MRC 时代,当我们自己创建了对象并把它作为方法的返回值返回出去时,需要手动调用对象的 autorelease 方法,如上节所讲的利用 autorelease 机制正确返回对象。到了 ARC 时代,ARC 需要保持对 MRC 代码的兼容,这就意味着 MRC 的实现和 ARC 的实现可以相互替换,而对象接收方和对象提供方无需知道对方是 MRC 实现还是 ARC 实现也能正确工作。比如,当基于 MRC 实现的代码调用你的一个 ARC 实现的方法来获取一个对象,那么你的方法必须同样采用上文所讲的 autorelease 机制来返回对象以确保对象接收方能正确获得对象。所以,即使在 ARC 模式下对象的 autorelease 方法不再能被显示调用,但是 autorelease 的机制仍然是在默默的工作着,只是编译器在帮你实践这一机制。

但是,ARC 还提出了巧妙的运行时优化方案来跳过 autorelease 机制。这个过程是这样的:当方法的调用方和实现方的代码都是基于 ARC 实现的时候,在方法 return 的时候,ARC 会调用objc_autoreleaseReturnValue() 替代前面说的 autorelease。在调用方持有方法返回对象的时候(也就是做 retain 的时候),ARC 会调用 objc_retainAutoreleasedReturnValue()。在调用 objc_autoreleaseReturnValue() 时,它会在栈上查询 return address 来确定 return value 是否会被传给 objc_retainAutoreleasedReturnValue()。如果没传,那么它就会走前文所讲的 autorelease 的过程。如果传了(这表明返回值能顺利从提供方交接给接收方),那么它就跳过 autorelease 并同时修改 return address 来跳过objc_retainAutoreleasedReturnValue(),从而一举消除了 autorelease 和 retain 的过程。这个方案可以在 MRC-to-ARC 调用、ARC-to-ARC 调用以及 ARC-to-MRC 调用中正确工作,并在符合条件的一些 ARC-to-ARC 调用中消除 autorelease 机制。

在访问 __weak 修饰的变量时,实际上必定会访问注册到 Autorelease Pool 的对象。如下来年两段代码是相同的效果:

 
  1. id __weak obj1 = obj0;
  2. NSLog(@"class=%@", [obj1 class]);
  3. // 等同于:
  4. id __weak obj1 = obj0;
  5. id __autoreleasing tmp = obj1;
  6. NSLog(@"class=%@", [tmp class]);

为什么会这样呢?因为 __weak 修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。

另一个隐式地使用 __autoreleasing 的例子就是使用 id 的指针或对象的指针(id *) 的时候。

看一个最常见的例子:

 
  1. NSError *__autoreleasing error;
  2. if (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) {
  3.   NSLog(@"Error: %@", error);
  4. }
  5. // 即使上面你没有写 __autoreleasing 来修饰 error,编译器也会帮你做下面的事情:
  6. NSError *error;
  7. NSError *__autoreleasing tempError = error; // 编译器添加
  8. if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError])
  9. {
  10.   error = tempError; // 编译器添加
  11.   NSLog(@"Error: %@", error);
  12. }

error 对象在你调用的方法中被创建,然后被放到 Autorelease Pool 中,等到使用结束后随着 Autorelease Pool 的销毁而释放,所以函数外 error 对象的使用者不需要关心它的释放。

在 ARC 中,所有这种指针的指针类型(id *)的函数参数如果不加修饰符,编译器会默认将他们认定为 __autoreleasing 类型。

有一点特别需要注意的是,某些类的方法会隐式地使用自己的 Autorelease Pool,在这种时候使用 __autoreleasing 类型要特别小心。比如 NSDictionary 的 enumerateKeysAndObjectsUsingBlock 方法:

 
  1. - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
  2. [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
  3. // do stuff
  4. if (there is some error && error != nil) {
  5. *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
  6. }
  7. }];
  8. }

上面的代码中其实会隐式地创建一个 Autorelease Pool,类似于:

 
  1. - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
  2. [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
  3. @autoreleasepool { // 被隐式创建。
  4. if (there is some error && error != nil) {
  5. *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
  6. }
  7. }
  8. }];
  9. // *error 在这里已经被dict的做枚举遍历时创建的 Autorelease Pool释放掉了。
  10. }

为了能够正常的使用 *error,我们需要一个 strong 类型的临时引用,在 dict 的枚举 Block 中是用这个临时引用,保证引用指向的对象不会在出了 dict 的枚举 Block 后被释放,正确的方式如下:

 
  1. - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
  2.   NSError * __block tempError; // 加 __block 保证可以在Block内被修改。
  3.   [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
  4.     if (there is some error) {
  5.       *tempError = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
  6.     }
  7.   }]
  8.   if (error != nil) {
  9.     *error = tempError;
  10.   }
  11. }

ARC 是在 iOS5 引入的,而 __unsafe_unretained 这个修饰符主要是为了在 ARC 刚发布时兼容 iOS4 以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义 property 时对应的是 unsafe_unretained__unsafe_unretained 修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。

__unsafe_unretained 的应用场景:

  • 在 ARC 环境下但是要兼容 iOS4.x 的版本,用 __unsafe_unretained 替代 __weak 解决强引用循环的问题。

ARC 模式下,还有一些需要注意的规则:

  • 不能显式使用 retain/release/retainCount/autorelease。
  • 不能使用 NSAllocateObject/NSDeallocateObject。
  • 需要遵守内存管理的方法命名规则。在 ARC 模式和 MRC 模式下,以 alloc/new/copy/mutableCopy 开头的方法在返回对象时都必须返回给调用方所应当持有的对象。在 ARC 模式下,追加一条:以 init 开头的方法必须是实例方法并且必须要返回对象。返回的对象应为 id 类型或声明该方法的类的对象类型,或是该类的超类型或子类型。该返回的对象并不注册到 Autorelease Pool 中,基本上只是对 alloc 方法返回值的对象进行初始化处理并返回该对象。需要注意的是:- (void)initialize; 方法虽然是以 init 开头但是并不包含在上述规则中。
  • 不要显式调用 dealloc。
  • 使用 @autoreleasepool 块替代 NSAutoreleasePool。
  • 不能使用区域(NSZone)。
  • 对象型变量不能作为 C 语言结构体(struct/union)的成员。
  • 显式转换 id 和 void *

Toll-Free Briding 保证了在程序中,可以方便和谐的使用 Core Foundation 类型的对象和Objective-C 类型的对象。

在 MRC 时代,由于 Objective-C 类型的对象和 Core Foundation 类型的对象都是相同的 release 和 retain 操作规则,所以 Toll-Free Bridging 的使用比较简单,但是自从切换到 ARC 后,Objective-C 类型的对象内存管理规则改变了,而 Core Foundation 依然是之前的机制,换句话说,Core Foundation 不支持 ARC。

这个时候就必须要要考虑一个问题了,在做 Core Foundation 与 Objective-C 类型转换的时候,用哪一种规则来管理对象的内存。显然,对于同一个对象,我们不能够同时用两种规则来管理,所以这里就必须要确定一件事情:哪些对象用 Objective-C(也就是 ARC)的规则,哪些对象用 Core Foundation 的规则(也就是 MRC)的规则。或者说要确定对象类型转换了之后,内存管理的 ownership 的改变。于是苹果在引入 ARC 之后对 Toll-Free Bridging 的操作也加入了对应的方法与修饰符,用来指明用哪种规则管理内存,或者说是内存管理权的归属。这些方法和修饰符分别是:

  • __bridge(修饰符)
  • __bridge_retained(修饰符) or CFBridgingRetain(函数)
  • __bridge_transfer(修饰符) or CFBridgingRelease(函数)

只是声明类型转变,但是不做内存管理规则的转变。

比如:

 
  1. CFStringRef s1 = (__bridge CFStringRef) [[NSString alloc] initWithFormat:@"Hello, %@!", name];

只是做了 NSString 到 CFStringRef 的转化,但管理规则未变,依然要用 Objective-C 类型的 ARC 来管理 s1,你不能用 CFRelease() 去释放 s1。

表示将指针类型转变的同时,将内存管理的责任由原来的 Objective-C 交给Core Foundation 来处理,也就是,将 ARC 转变为 MRC。

比如,还是上面那个例子

 
  1. NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
  2. CFStringRef s2 = (__bridge_retained CFStringRef)s1;
  3. // or CFStringRef s2 = (CFStringRef)CFBridgingRetain(s1);
  4. // do something with s2
  5. //...
  6. CFRelease(s2); // 注意要在使用结束后加这个

我们在第二行做了转化,这时内存管理规则由 ARC 变为了 MRC,我们需要手动的来管理 s2 的内存,而对于 s1,我们即使将其置为 nil,也不能释放内存。

这个修饰符和函数的功能和上面那个 __bridge_retained 相反,它表示将管理的责任由 Core Foundation 转交给 Objective-C,即将管理方式由 MRC 转变为 ARC。

比如:

 
  1. CFStringRef result = CFURLCreateStringByAddingPercentEscapes(. . .);
  2. NSString *s = (__bridge_transfer NSString *)result;
  3. //or NSString *s = (NSString *)CFBridgingRelease(result);
  4. return s;

这里我们将 result 的管理责任交给了 ARC 来处理,我们就不需要再显式地将 CFRelease() 了。

对了,这里你可能会注意到一个细节,和 ARC 中那个 4 个主要的修饰符(__strong, __weak, ...)不同,这里修饰符的位置是放在类型前面的,虽然官方文档中没有说明,但看官方的头文件可以知道。记得别把位置写错。

image

参考:

  • Objective-C Automatic Reference Counting (ARC)
  • Transitioning to ARC Release Notes
  • iOS开发ARC内存管理技术要点
  • Objective-C Boot Camp

这篇关于iOS ARC 内存管理要点的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/949484

相关文章

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

【iOS】MVC模式

MVC模式 MVC模式MVC模式demo MVC模式 MVC模式全称为model(模型)view(视图)controller(控制器),他分为三个不同的层分别负责不同的职责。 View:该层用于存放视图,该层中我们可以对页面及控件进行布局。Model:模型一般都拥有很好的可复用性,在该层中,我们可以统一管理一些数据。Controlller:该层充当一个CPU的功能,即该应用程序

Sentinel 高可用流量管理框架

Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。 Sentinel 具有以下特性: 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

JVM 常见异常及内存诊断

栈内存溢出 栈内存大小设置:-Xss size 默认除了window以外的所有操作系统默认情况大小为 1MB,window 的默认大小依赖于虚拟机内存。 栈帧过多导致栈内存溢出 下述示例代码,由于递归深度没有限制且没有设置出口,每次方法的调用都会产生一个栈帧导致了创建的栈帧过多,而导致内存溢出(StackOverflowError)。 示例代码: 运行结果: 栈帧过大导致栈内存