ARC环境下iOS内存管理总结

自动引用计数,又称ARC(Automatic Reference Counting)是苹果在iOS5中引入的重要特性.

基于引用计数的内存管理

要了解ARC,必须先了解Objective-C中对象的内存管理机制以及手动管理引用计数(MRR,Manual Retain-Release)。

**Objective-C内存管理机制 **

Objective-C中的对象都是基于引用计数来管理生命周期的。简单来说就是,我们在需要持有一个对象时,调用retain让它的引用计数+1。不需要这个对象的时候,调用release让它的引用计数-1。当一个对象引用计数为0的时候,这个对象就会被自动销毁。

MRR

我们在手动管理引用计数的时候,要明确地控制对象的生命周期,显式的调用每一个retain和release。我们必须清楚的了解每个接口对引用计数的处理(如把一个对象放到数组里引用计数会被+1,用alloc创建的对象的引用计数一开始就是1,用哪些接口创建的对象是已经被调用过autorelease的等等)。在处理引用计数时稍有疏忽,就可能导致程序崩溃或内存泄漏。

ARC

ARC是编译器通过对代码的静态分析,确定对象的生命周期,并在合适的位置自动加上retain和release的机制。把内存管理交给编译器以后,我们不需要再调用任何的retain和release了。ARC减少了MRR带来的思考负担,减少了内存问题出现的可能性,也大幅减少了代码量。

下图是苹果的文档中对ARC效果的描述,略显夸张的展示出了ARC的好处


ARC环境下iOS内存管理总结_第1张图片
ARC_Illustration.jpg

ARC的内存管理

_ARC简化了引用计数的概念,它把变量对对象的引用分为强引用和弱引用两种。所有ARC下的内存管理原则都将基于这两个概念。 _

强引用表示变量拥有对象的所有权。多个变量有可能同时持有同一个对象的强引用。当一个对象没有被任何变量强引用,它就会被释放。

弱引用表示引用但不拥有对象,它可以避免两个对象相互强引用导致的内存泄漏问题。

  • 所有权修饰符
    ARC下的变量都会被加上下面几种所有权修饰符,它们指定了变量对其指向对象的所有权形式。整个ARC的规则也正是围绕着这几个修饰符运作:
    __strong:表示对对象的强引用。它是变量修饰符的默认值,也就是说只要没有显式的给变量加上所有权修饰符,它就是__strong的。__strong变量在离开作用域范围后会被废弃(其实就是编译器插入了一个release),对对象的强引用也随之消失。
    __weak:表示弱引用。当其指向的对象被释放时,这个变量会被置成nil。
    __autoreleasing:表示将修饰的对象加入autoreleasepool中,在autoreleasepool被销毁时自动释放对对象的强引用。在@autoreleasepool的代码块中的变量都会被加上这个修饰符,在超出代码块范围后释放。详见下面autorelease的部分。
    __unsafe_unretained:表示既不持有对象的强引用,也不持有弱引用(对象析构时它不会被置为nil)。正如它的名字描述,它是不安全的。它只是在iOS5之前用来代替__weak。

  • 属性修饰符
    在使用MRR的时候,我们可以给property添加retain、assign、copy这几种修饰符,来设置想要的内存管理模式,用下面这段代码来说明这三种修饰符的作用:

// MRR环境
@property (nonatomic, retain) NSObject* retainedObject;
@property (nonatomic, assign) NSObject* assignedObject;
@property (nonatomic, copy) NSMutableString* copiedObject;

- (void)testProperties {
  NSObject* objectA = [[NSObject alloc] init]; // objectA引用计数为1。
  self.retainedObject = objectA; // self.retainedObject和objectA指向同一个对象,且该对象引用计数为2。
 
  NSObject* objectB = [[NSObject alloc] init]; // objectB引用计数为1。
  self.assignedObject = objectB; // self.assignedObject和objectB指向同一个对象,且该对象引用计数为1。
    
  NSMutableString* objectC = [[NSMutableString alloc] initWithFormat:@"test"]; // objectC引用计数为1    
  self.copiedObject = objectC; // self.copiedObject和objectC指向两个不同的对象,两个对象引用计数都为1。

  // 这里可能会有疑问,为什么copy的例子用的是NSMutableString而不是NSObject了或者NSString?
  // 因为NSObject没有实现NSCopying协议,没法复制。
  // 而使用NSString会导致self.copiedObject和objectC因为编译器优化而指向相同的对象。
    
  // 因为是MRR环境,我们要释放我们自己分配的内存,否则会产生内存泄漏。
  [objectA release];
  [objectC release];
  // 当然,给分配内存的代码加上autorelease也行:
  // NSObject* objectA = [[[NSObject alloc] init] autorelease];
  // NSMutableString* objectC = [[[NSMutableString alloc] init] autorelease];
}

- (void)dealloc {
  // MRR要在析构时手动清理内存。
  [_objectA release];
  [_objectB release];
  [_objectC release];

  [super dealloc];
}

在ARC环境下,增加了两种新的修饰符:strong和weak,分别对应强引用和弱引用:

// ARC环境
@property (nonatomic, strong) NSObject* strongObject;
@property (nonatomic, weak) NSObject* weakObject;

- (void)testProperties {
  NSObject* objectD = [[NSObject alloc] init]; // objectD持有对象D的强引用。
  self.strongObject = objectD; // self.strongObject和objectD都持有对象D的强引用

  NSObject* objectE = [[NSObject alloc] init]; // objectE持有对象E的强引用。
  self.weakObject = objectE; // self.weakObject持有对象E的弱引用

  // 在ARC环境下,这个方法执行完后,objectD和objectE对对象D和E的强引用会消失。
  // 这时候self.strongObject仍然持有对对象D的强引用。
  // self.weakObject之前对对象E持有的是弱引用,对象E析构。self.weakObject的指针被置为nil。
}

// 与MRR不同,ARC环境下,self对象析构时,self.strongObject对对象E的强引用自动消失,对象E自动析构。

Retain Cycle(保留环)

ARC确实帮助我们避免了许多内存管理的问题。但在ARC环境下,有一类问题还是需要被妥善的处理,这类问题叫做Retain Cycle(保留环)。

ARC环境下的对象在没有被强引用时就会被释放,当两个对象互相对对方持有强引用时,这两个对象就永远不会被释放了。这就导致了上面说的保留环问题。

要解决保留环的思路也简单,就是理清这两个对象之间的所有权关系,再让其中一个对象对另一个对象持有弱引用(使用weak指针)即可。

有一种保留环的问题相对隐蔽,出现在使用block的时候。block是一个可以被独立运行的代码块,为了保证它随时可以被运行,它会持有对它包含的所有变量的强引用。我们来看有问题的代码:

@property (nonatomic, strong) ExampleBlock aBlock; // self持有aBlock对象的强引用

- (void)exampleFunction {
  self.aBlock = ^{
      [self doSomething]; // aBlock持有self的强引用
  };
}

上面的代码中,self和aBlock两个对象互相持有对方的强引用,导致了两个对象都无法被释放。

我们在遇到上述情况时,要让其中一个引用变为弱引用,修改后的代码如下:

@property (nonatomic, strong) ExampleBlock aBlock;

- (void)exampleFunction {
  // 让block捕获self的弱引用
  __weak __typeof(self)weakSelf = self; 
  self.aBlock = ^{
    // 把弱引用转化为强引用,防止在block处理过程中self被析构。
    __strong __typeof(weakSelf)strongSelf = weakSelf; 
    [strongSelf doSomething];
  }];
}

autorelease

在iOS中,autorelease的意义是稍后释放。我们先看一段MRR的代码:

// MRR
- (void)doSomething {
  NSArray* arrayA = [[NSArray alloc] init];
  NSArray* arrayB = [self getEmptyArray];

  // 按照MRR的原则,创建的对象的地方必须负责对象的释放。这样才能保持引用计数的平衡。
  // 所以这里必须调用[arrayA release];来与上面的alloc保持平衡。
  [arrayA release]; 
  // 然而arrayB是从getEmptyArray方法中得来,
  // 我们并不知道getEmptyArray是创建了一个新的对象还是返回了某个类的成员变量,
  // 这里释放getEmptyArray返回的对象是不合适的。
}

- (void)getEmptyArray {
  // 我们必须在getEmptyArray的实现中来保证引用计数的平衡。
  // 如果写return [[[NSArray alloc] init] release];会返回一个nil。
  // 所以,用autorelease让新创建的对象进行稍后释放。
  return [[[NSArray alloc] init] autorelease];
}

稍后到底是什么时候?这个涉及到autoreleasepool的概念。
当一个对象被autorelease的时候,它其实是被注册到最里层(autoreleasepool是类似栈的结构)的autoreleasepool里,在autoreleasepool被销毁的时候,里面所有的对象都会被销毁。
系统会在每次消息循环开始的时候,建立一个autoreleasepool,在这一次消息循环结束后销毁这个autoreleasepool。一般情况下,这个就是最里层的autoreleasepool了。

我们可能会在一次消息循环周期内创建大量的autorelease对象。为了防止内存占用过多,我们可以手动使用@autoreleasepool代码块来创建自己的autoreleasepool,让我们创建的这些对象提前释放:

- (void)autoreleasePoolExample {
  @autoreleasepool {
    // 在这个块范围内被autorelease的对象会加到这个新的autoreleasepool里,
    // 在这个代码块结束时就被释放。
  }
}

在ARC下我们不能显式调用autorelease方法,那autorelease到底会在什么时候用到呢?其实编译器已经帮我们加上了autorelease:

- (void)getEmptyArray {
  // 在ARC下我们不能自己加上autorelease。编译器为保证平衡和返回值的有效性,
  // 会给这个方法的返回值隐式的加上autorelease。
  // 实际上,除了alloc/new/copy等开头的方法外,
  // 其它方法的返回值都会按照这个规则,被自动加上autorelease。
  return [[NSArray alloc] init];
}

你可能感兴趣的:(ARC环境下iOS内存管理总结)