前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈 iOS ARC 内存管理

浅谈 iOS ARC 内存管理

作者头像
s_在路上
发布2019-03-04 15:02:53
1.4K0
发布2019-03-04 15:02:53
举报
文章被收录于专栏:iOS 开发杂谈

Objective-C 采用的是引用计数式的内存管理方式:

  • 自己生成的对象自己持有。
  • 非自己生成的对象自己也能持有。
  • 自己持有的对象不再需要时释放。
  • 非自己持有的对象自己无法释放。

在 ARC 环境下,id 类型和对象类型和 C 语言其他类型不同,类型前必须加上所有权的修饰符。 所有权修饰符总共有4种:

  • __strong
  • __weak
  • __autoreleasing
  • __unsafe_unretained

__strong

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

__weak

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。__weak 最常见的一个作用就是用来避免强引用循环。

__weak 的几个使用场景:

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

另外,__weak 修饰符的变量,会被注册到 autoreleasePool 中。

代码语言:javascript
复制
{
    id __weak obj1 = obj;
    NSLog(@"obj2-%@",obj1);
}

编译器转换上述代码如下:

代码语言:javascript
复制
id obj1;
objc_initweak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);

objc_loadWeakRetained 函数获取附有 __weak 修饰符变量所引用的对象并 retain, objc_autorelease 函数将对象放入 autoreleasePool 中,据此当我们访问 weak 修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用 weak 的话,在我们去访问 weak 修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能。

解决方案: 要访问 weak 修饰的变量时,先将其赋给一个 strong 变量,然后进行访问。

为什么访问 weak 修饰的对象就会访问注册到自动释放池的对象呢?

因为 weak 不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在 autoreleasePool 销毁时释放对象占用的内存。

__autoreleasing

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

__autoreleasing 修饰的对象会被注册到 Autorelease Pool 中,并在 Autorelease Pool 销毁时被释放。

注意:定义 property 时不能使用这个修饰符,因为任何一个对象的 property 都不应该是 autorelease 类型的。

__unsafe_unretained

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

__unsafe_unretained 的应用场景:

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

最后

总结, autorelease 的机制却依然在很多地方默默起着作用,我们来看看这些场景:

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

方法返回值

首先,我们看这个方法:

代码语言:javascript
复制
-  (NSObject *)object  {
    NSObject *obj = [[NSObject alloc] init];
    return obj;
}

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

方法返回值时的 autorelease 机制

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

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

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

访问 __weak 修饰的变量

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

代码语言:javascript
复制
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

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

id 的指针或对象的指针(id *)

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

看一个最常见的例子:

代码语言:javascript
复制
NSError *__autoreleasing error;
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) {
      NSLog(@"Error: %@", error);
}
// 即使上面你没有写 __autoreleasing 来修饰 error,编译器也会帮你做下面的事情:
NSError *error;
NSError *__autoreleasing tempError = error; // 编译器添加
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError])
{
      error = tempError; // 编译器添加
      NSLog(@"Error: %@", error);
}

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

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

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

代码语言:javascript
复制
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        // do stuff
        if (there is some error && error != nil) {
            *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
        }
        
    }];
    }
}

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

代码语言:javascript
复制
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        @autoreleasepool {  // 被隐式创建。
            if (there is some error && error != nil) {
                *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
            }
                      }
    }];
    // *error 在这里已经被dict的做枚举遍历时创建的 Autorelease Pool释放掉了。
    }
}

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

代码语言:javascript
复制
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
      NSError * __block tempError; // 加 __block 保证可以在Block内被修改。
      [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            if (there is some error) {
                  *tempError = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
                } 
          }]
      if (error != nil) {
            *error = tempError;
          } 
}

参考资料 http://www.samirchen.com/ios-arc/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.02.13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • __strong
  • __weak
  • __autoreleasing
  • __unsafe_unretained
  • 最后
    • 方法返回值
      • 方法返回值时的 autorelease 机制
    • 访问 __weak 修饰的变量
      • id 的指针或对象的指针(id *)
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档