ARC环境下iOS内存管理总结

自动引用计数,又称ARC(Automatic Reference Counting)是苹果在iOS5中引入的重要特性,它减少了我们在内存管理时的麻烦,让我们可以把更多的精力放在其它更重要的事情上。

虽然ARC给我们带来了很多方便,但如果开发者不了解基本的内存管理知识,还是会在开发工作中遇到很多问题。所以,我总结了ARC环境下应该知道的内存管理知识,供诸位参考。


基于引用计数的内存管理

要了解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的内存管理

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];
}

参考文档

Objective-C高级编程:iOS与OS X多线程和内存管理

Advanced Memory Management Programming Guide
Transitioning to ARC Release Notes
Clang:Objective-C Automatic Reference Counting (ARC)
ARC Best Practices
黑幕背后的Autorelease

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容