ARC系列一:ARC VS MRC

557 阅读8分钟

1. 前言

本篇文章是对 Automatic reference counting(ARC) 的介绍的第一篇文章,主要介绍 Manual reference counting(MRC)和ARC之间区别和联系。注意的是本文提到的消息和方法为同样的意思,可以理解为函数调用。

2. 内存分布

一个程序有自己的内存空间,也即虚拟地址空间。一个程序的数据和对象都存在于该虚拟地址空间中。但是该虚拟地址空间并不是无限大的,这就意味着并不能无限生成对象,也就是资源有限。那么在该虚拟内存中主要有两部分存储对象:栈和堆。其中栈内存是由编译器自动管理,而堆内存需要程序员手动管理。如果程序对堆内存使用不当,那么就会造成内存泄露。

3. Objective-C中的引用计数

3.1 什么叫引用计数?

引用计数是一种内存管理技术,大概可概括成:当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。 从下图图片中显示,生成一个NSString对象(可能这样不太恰当,但是比较直观,此时只需将NSString看作一个普通对象),此时有一个NSString *指针引用它,它的引用计数为1。其中ptr1分配在栈内存上,而NSString对象分配在堆内存上。(此时无需考虑是MRC还是ARC,下面例子的代码只是为了方便介介绍引用计数的概念)

NSString *ptr1 = [NSString stringWithString:@"str"];

未命名文件.png

当我们写下如下代码,

NSString *ptr2 = ptr1;

此时引用计数的情况变成了如下图所示,从图片中可以看出,此时NSString对象有两个指针引用它,那么它的引用计数为2。

未命名文件 (2).png

那么当执行如下代码,

ptr1 = nil;

ptr2 = nil;

此时引用计数的情况如下图所示,此时堆内存对象的引用计数为0,则对象的堆内存将会被系统回收。

未命名文件 (3).png

3.2 Objective-C中的内存管理策略

3.2.1 基本的内存管理原则

内存管理模型基于对象所有权。一个堆对象可能有一个或多个所有者。只要一个对象至少有一个所有者持续存在。如果一个对象没有了所有者,即引用计数为0,runtime system将会自动销毁该对象。在编写代码的时候请想好两个问题,什么时候你拥有该对象的所有权,什么时候你失去该对象的所有权。CoCo设置了下面几个原则,其实也就是你在MRC环境下,自己应该遵循什么原则来编写代码。

  1. 你创建的对象,则你拥有该对象的所有权

    比如使用alloc,new,copy和mutableCopy等方法创建的对象,表示你拥有所有权。当你在使用上面提到的方法创建的对象,你知道此时你拥有该对象的所有权,那么当你不再使用该对象时,你应该释放所有权。也就是使用release。

{
    NSSting *str = [NSString alloc] init];
    //do something
    [str release];
}
  1. 你可以使用retain来获取某个对象的所有权。

    某个方法返回的对象能保证在该方法中,该对象有效。并且能够安全的将对象返回给调用者。你可以在两种情况下使用retain:(1)在访问器或init方法中,当你存储属性值,该属性指向一个堆对象,可以获得该对象的所有权(2)防止其它操作的导致对象被销毁。

  2. 当你不在需要拥有所有权的对象时,可以放弃所有权。

    通过release或autorelease来释放对象的所有权。

  3. 你不准对一个没有所有权的对象,进行释放所有权操作。

     获取所有权可以对对象发送`retain`消息,而释放所有权则可以发送’release‘或`autorelease`(延迟release)消息。注意的是release消息并不是销毁对象,只是将对象的引用计数减1。对象的内存释放需要等其引用计数为0。其中retain和release消息介绍如下:
    
  • retain taking no arguments and returning a pointer to the object.
  • release, taking no arguments and returning void. 当显式的发送retain,release和autorelease消息的代码均为非ARC环境。因为在ARC环境下,不允许显式的对对象发送这些消息。

3.2.2 一个例子

本小节主要介绍在MRC的情况下,程序员如何编写管理对象生命周期的代码。

{
    Person *aPerson = [[Person alloc] init];
    //...
    NSString *name = aPerson.fullName;
    //...
    [aPerson release];
}

根据命名规则,当程序员创建Person对象,而Person对象创建自alloc,所以它是自己创建并拥有所有权,并且将指针值赋值给aPerson。所以当不需要它时,需要对其发送release消息(MRC)。因为person’s name不是从owning methods(指的是对其发送了retain的方法)中返回,所以不要对其发送release消息。

3.2.3 使用autorelease来发送一个延迟的release消息

当你需要返回一个对象,该对象从别的方法生成,则此时使用autorelease。如下情况所示:

-(NSString *)fullName{
    NSString *str = [NSString alloc] initWithFormat:"%@ %@", self.firstName, self.lastName] autorelease];
    
    return str;
}

你拥有指针str所指向的对象,因为它由alloc生成。但是你必须释放对它的所有权,当str销毁时,也即该函数执行结束。如果使用release,那么该对象将会被销毁在它调用之前,那么该方法将会返回一个无效的对象。此时应该使用autorelease,来延缓该对象的释放。当你调用该方法时,可以获得一个有效的对象,但是并不拥有其所有权。所以需要发送retain消息。

{
    //此时,fullName并不拥有该对象的所有权
    NSString *fullName = [Person fullName];
    //执行retain,拥有该对象的所有权。
    [fullName reatin];
    //...
    
    //释放对该对象的所有权
    [fullName release];
   
}

你也可以将fullName的代码写成如下:

- (NSString *)fullName{
    NSString *str = [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
    
    return str;
}

因为stringWithFormat方法并不符合alloc,new,copy和mutableCopy等命名约定,所以它返回的对象指针并不持有该对象。那么当你掉用该fullName方法,需要对其进行retain操作,不再使用时,执行release操作。下面的写法就是错误的。

-(NSString *)fullName{
    //1. 返回获得所有权的对象指针,此时该对象的引用计数为1。
    NSString *str = [NSString alloc] initWithFormat:"%@ %@", self.firstName, self.lastName];
    
    return str;
}

根据命名的约定,fullName的调用者根据名字,会觉得对返回的字符串没有拥有所有权,因此调用者可能写下下面的代码。

{
    NSString *name = [aPerson fullName];
    //2. 根据名字约定,要对其retain操作,获得该对象的所有权(这是调用者主观认为的,实际上返回的指针已经获得对象的所有权),该对象的引用计数为2。
    [name retain];
    //... do something
    
    //3. 执行release操作,将对象的引用计数减去1。此时该对象的引用计数为1。
    [name release];
}

从上面的例子可以看出,fullName中生成的对象最终的引用计数为1,这就造成了内存泄漏。所以在MRC环境下,必须遵循命名约定来实现方法,否则可能会造成内存泄露。

3.2.4 执行dealloc来放弃对象的所有权

如果定义一个类,该类有一个或多个属性。那么需要在dealloc方法中对这些属性执行release操作,释放其引用的对象的所有权。NSObject类中定义了一个方法dealloc,当一个对象没有所有者时,该方法会自动调用。dealloc的作用是释放对象自己所占的内存,并且释放其持有的资源,包括任何对象的实例变量的所有权。

@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end

@implement Person
//...
- (void)dealloc{
    [_firstName release];
    [_lastName release];
    [super dealloc];
}

需要注意的是永远不要直接调用dealloc,在子类的dealloc中的最后必须调用父类的dealloc方法,

当应用程序终止时,可能不会向对象发送dealloc消息。 因为进程的内存会在退出时自动清除,所以简单地让操作系统清理资源比调用所有内存管理方法更有效。

4. ARC

在编译其中开启ARC,项目的build setting中找到Objective-C Automatic Reference Counting可以开启ARC,当然现在的Xcode版本都是默认开启ARC。 ARC仍然遵循MRC的内存管理的几个基本原则,只是开启了ARC之后,就无需程序员手动编写retian等引用计数相关的代码。

这里与上文一个手动引用计数的例子进行比较。 对于fullName方法的调用

-(NSString *)fullName{
    NSString *str = [NSString alloc] initWithFormat:"%@ %@", self.firstName, self.lastName] autorelease];
    
    return str;
}

在MRC情况下:

{
    NSString *name = [aPerson fullName];
    //.... do something
    //手动编写
    [name release];
}

在ARC情况下:

{
    NSString *name = [aPerson fullName];
    //.... do something
    //无需手动编写,release方法的调用由编译器自动完成。
    //[name release];
}

5 总结

  • 资源有限,所以需要内存管理
  • Objective-C对象使用基于引用计数管理的方法
  • 内存对象管理基于四个原则
  • MRC需要手动编写retain,release和autorelase方法
  • 编译器开启ARC后,程序员无需手动编写retain,release和autorelease方法,这一切由编译器完成。

参考

  1. 引用计数
  2. Memory Management Policy
  3. Automatic Reference Counting