ARC vs MRC,这不是一个编程习惯问题

image

引言

虽然距离WWDC2011和iOS5已经过去3年多时间了,但之前我一直没有去研究过ARC,一个是因为觉得ARC非常简单,到时什么时候想使用ARC时再转向ARC,第二个是担心内存管理不受自己控制(好吧,从现在来看是我对ARC机制了解不足而已),第三,我觉得使用MRC更能体现一个码农对内存管理的理解,第四,我是一个追求技术不“追赶时髦”的码农。说了这么多的原因,其实是想说作为一个最需要“追求时髦”的职业,我这样的心态是错误的。写这篇文章的目的是希望和我一同处于对 MAC & ARC迷茫的人的一个参考吧!

目录

  • 1、 什么是ARC
  • 2、 ARC工作原理
  • 3、 MRC的代价
  • 4、 ARC & MAC 在大量数据下的测试
  • 5、 CF与Objective-C在ARC下的内存管理
  • 6、 使用ARC注意事项
  • 7、 总结

1、什么是ARC

什么是ARC,google一下,你会发现有太多太多对ARC非常非常详细的讲解。对于有C++背景的人来说,ARC的本质从某种角度上来说类似 C++ 的智能指针,区别就是ARC更智能简单,而且会加速程序,而不是像智能指针那样会一定程度上减慢程序运行。对于纯ObjC背景的人来说,ARC相当于编译器自动帮你填写了 retain, release。但是,远远不是这么简单。

首先ARC不会真的填写retain/releaseretain/release 是 ObjC的消息,ARC会直接调用runtime的C函数,这会快很多。另外对于MRC中恶心的 return [[[XXX alloc] init] autorelease] ,ARC不但可以简化其写法,还可以让它更快,原因就在于它可以消除不必要的“入池”操作(autorelease是放到了自动释放池),详见objc_retainAutoreleasedReturnValue.

基于上面两点,ARC会让所有涉及到内存的操作变快。

ARC虽然会让单位内存操作变快,甚至会智能的取消某些retain/release,但是毕竟ARC不是人脑,如果一个人完全清晰的掌握某个对象的生命周期,那么他完全可以只retain一次,然后在最后不需要的时候release掉,所以MRC可以在这种情况下比ARC快。至于具体应用到项目中的数据,可以参考 http://www.learn-cocos2d.com/2013/12/performance-comparison-cocos2diphone-v2-v3-sparrow-arc-mrc/ 。其中有快有慢。

2、 ARC工作原理

手动内存管理的机理大家应该已经非常清楚了,简单来说,只要遵循以下三点就可以在手动内存管理中避免绝大部分的麻烦:

如果需要持有一个对象,那么对其发送retain 如果之后不再使用该对象,那么需要对其发送release(或者autorealse) 每一次对retain,alloc或者new的调用,需要对应一次release或autorealse调用

初学者可能仅仅只是知道这些规则,但是在实际使用时难免犯错。但是当开发者经常使用手动引用计数 MRC 的话,这些规则将逐渐变为本能。你会发现少一个release的代码怎么看怎么别扭,从而减少或者杜绝内存管理的错误。可以说MRC的规则非常简单,但是同时也非常容易出错。往往很小的错误就将引起crash或者内存溢出之类的严重问题。

在MRC的年代里,为了避免不小心忘写release,Xcode提供了一个很实用的小工具来帮助可能存在的代码问题(Xcode里默认快捷键command+B),可以指出潜在的内存泄露或者过多释放。而ARC在此基础上更进一步:ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做的只不过是在代码编译时为你自动在合适的位置插入release或autorelease,就如同之前MRC时你所做的那样。

3、MRC的代价

代码有好多代价,最简单直白的代价是编写时的代价,然后更重大的代价则是维护的代价。

编写的代价: 每个人的脑力都是有限的,而在编程的时候往往需要全心专注,这说明编程本身就耗费了100%的脑力。基于这个出发点,那么如果一个人在每写100行代码里面10行都是内存维护相关的代码时,他分配给其他的东西(程序结构,API设计,业务逻辑)肯定会减少,除非他愿意花更多的时间来写这个东西(加班)。注意,内存维护的10行代码并非简单地事情,要把他们搞正确,一个合格的MRC程序员肯定会前后审阅自己的代码好几遍。

维护的代价: 代码的本质是动态的,它会随着时间不停的改变自己,所以代码不但需要运行时健壮,同时还需要重构健壮,即你能安全的重构一段代码,而不是重构之后错误百出。这个举个常见例子:

在MRC下,有一个函数,在运行中间会 return 掉,那么所有合格的MRC程序员必然会记得在return之前把 alloc 的对象逐个 release 掉,咱不讨论在MRC下如果多几个中途return会让代码多么难写(这是编写代价),假设写好了,程序OK,没bug。然后某天重构了,把 return 提前了,然后由于位置提前,需要release的对象变成了另外一些,这会造成相当多的重构bug。另外一个例子,假设这个MRC程序员采用了极端的 retain/release 优化,那么在重构时必然要全面审视新的代码下面原来的优化是否安全,代价很高。那如果这些代码要交给别人重构呢?

维护代价的另一面是阅读时的代价。代码的价值是给人(别人或者自己)读,一行代码敲下去,可能要被读10遍,20遍。设想一下阅读到处穿插 retain/ release 的代码 vs 阅读清晰的业务逻辑的代码的容易程度对比。

4、 ARC & MAC 在大量数据下的测试

下面举一个例子,同样的代码,只是在「ARC」与「MRC」的情況下编译执行, 但是二者所需要的时间是相差数倍的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int n = 600000, m = 10000000;
arr = [NSMutableArray new];
for (int i = 0; i < n; i++) {
    [arr addObject:[NSNumber numberWithInt:1]];
}

CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();

id obj = nil;
for (int j = 0; j < m; j++) {
    obj = [arr objectAtIndex:n - 1];
}

CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();

NSLog(@"end:%lf, start:%lf, diff:%lf", end, start, end - start);

[arr release]; // -> 这一行是「MRC」需要加上的,但「ARC」沒有这行

执行结果:

MRC:  end:358178433.846184, start:358178433.753032, diff:0.093152
ARC:    end:358178929.480108, start:358178928.841418, diff:0.638690

可以看出「MRC」的版本比「ARC」的版本快了近7倍!

但实际上程序是慢在哪呢? –> 慢在 obj = [arr objectAtIndex:n – 1]; 这个地方, XCode 在 Compile 的时候,帮我們加上了类似下面的代码,

obj = [[arr objectAtIndex:n - 1] retain];
[obj autorelease];

如果把「MRC」的版本修改为上述的代码, 则执行結果:

MRC:  end:358179022.496308, start:358179021.894909, diff:0.601399

是不是就变慢了! 因此,XCode 在 Compile 的时候,我想它对代码的记忆管理是采取较保守的态度, 如此看來,iOS 5 预设 property 为 strong 也就不意外了!

那么上面所举的例子要怎么解決呢? –> 我们可以透过 Toll-Free Bridged Types 来解決! 来看一下,我们将 ARC 的版本的代码改成下面这个样子: 將NSArray改成使用CFArrayRef, 这是 Foundation class –> Core Foundation type 的转换, 这样的转换是 Toll-Free 的!

__unsafe_unretained id obj;
for (int j = 0; j < m; j++){
  obj = (__bridge __unsafe_unretained id)CFArrayGetValueAtIndex((__bridge CFArrayRef)arr, n - 1);
}

如果把「ARC」的版本修改为上述的代码, 则执行结果:

MRC:  end:358179460.237259, start:358179460.004701, diff:0.232558

是不是就变快了!(但还沒有办法跟原本的「MRC」版本一样快!) 所以其实写程序的時候要多想一下有沒有其它作法, 因为不同的写法虽然可能是相同結果, 但所需要的时间是不同的, 在使用 ARC 時, 如果能清楚的知道自己所创建的物件是被 retain 的状态, 那么在传递的过程中就可以视需求决定接收此物件是要 retain 或只是 assign, 这样可以让 XCode 在 Compile 的时候, 依照我们給它的指示去产生记忆体管理的代码, 避免不必要或多余的效能损失!

5、 CF与Objective-C在ARC下的内存管理

在cocoa application的应用中,我们有时会使用Core Foundation(CF),我们经常会在Objective-C和CF之间进行转化。系统使用arc的状态下,编译器不能自动管理CF的内存,这时候你必须使用CFRetain和CFRelease来进行CF的内存的管理。

具体的CF内存管理规则见: Memory Management Programming Guide for Core Foundation

在OC和FC之间进行转化的时候,主要是对象的归属问题。共有两种方式:

1、使用宏,可以标识归属者从OC到CF,还是从CF到OC。

1
2
3
4
5
6
7
NS_INLINE CFTypeRef CFBridgingRetain(id X) {
  return (__bridge_retain CFTypeRef)X;
}

NS_INLINE id CFBridgingRelease(CFTypeRef X) {
  return (__bridge_transfer id)X;
}

2、使用转化符,如:__bridge__bridge_transfer__bridge_retained

1
2
3
4
5
6
7
8
id my_id;
CFStringRef my_cfref;

NSString   *a = (__bridge NSString*)my_cfref;     // Noop cast. 
CFStringRef b = (__bridge CFStringRef)my_id;      // Noop cast. 

NSString   *c = (__bridge_transfer NSString*)my_cfref; // -1 on the CFRef 
CFStringRef d = (__bridge_retained CFStringRef)my_id;  // returned CFRef is +1

下面以详细的例子来介绍一下OC和CF在arc下内存管理的详细写法.下面以CFURLCreateStringByAddingPercentEscapes()函数为例说一下在ARC下的写法和非ARC下的写法。

非ARC模式下的写法:

1
2
3
4
5
6
7
8
#pragma mark – View lifecycle 
- (void)viewDidLoad {
  [super viewDidLoad];
  NSLog(@"=%@", [self escape:@"wangjun"]);
}
-(NSString *)escape:(NSString *)text {
  return (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,(__bridge CFStringRef)text, NULL, CFSTR("!*’();:@&=+$,/?%#[]"), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));;
}

使用instruments检测,没有内存泄漏。

下面把上面工程改为arc模式。

可以看到xcode自动把上面函数转化为:

1
2
3
4
5
6
7
8
#pragma mark – View lifecycle 
- (void)viewDidLoad {
  [super viewDidLoad];
  NSLog(@"=%@", [self escape:@"wangjun"]);
}
-(NSString *)escape:(NSString *)text {
  return (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,(__bridge CFStringRef)text,NULL,CFSTR("!*’();:@&=+$,/?%#[]"), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
}

在arc中,CF和OC之间的转化桥梁是 __bridge,有两种方式:

  • __bridge_transfer ARC接管管理内存
  • __bridge_retained ARC释放内存管理

上面的方法是从CF转化为OC NSString对象,使用的__bridge_transfer ,对象所有者发生转变,由CF到OC,最后由ARC接管内存管理。运行上面的代码,用instruments检测,是没有内存泄漏的。

上面代码等同于:

1
2
3
- (NSString *)escape:(NSString *)text {
  return CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)text,NULL,CFSTR("!*’();:@&=+$,/?%#[]"), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)));
}

如果将上述代码改为:

1
2
3
-(NSString *)escape:(NSString *)text {
  return (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,(__bridge CFStringRef)text, NULL, CFSTR("!*’();:@&=+$,/?%#[]"), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));;
}

编译也会成功,但是这时候用instruments检测,可以发现内存泄漏:

由于CF转化完OC,没有自己释放内存,同时也没有把内存管理交给ARC,所以出现内存泄漏。由于__bridge只是同一个对象的引用,内存的所有权没有发生变化。

下面在说一下oc到CF的转化,需要把OC的内存管理权释放掉。

1
2
3
4
NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
CFStringRef s2 = (__bridge_retained CFStringRef)s1;
// do something with s2 // . . . 
CFRelease(s2);

最后由CF进行内存释放。

上面代码等同于:

1
2
3
CFStringRef s2 = CFBridgingRetain(s1);
// . . . 
CFRelease(s2);

下面总结一下我们使用ARC情况下。oc和CF互相转化的原则:

  • CF转化为OC时,并且对象的所有者发生改变,则使用CFBridgingRelease()__bridge_transfer
  • OC转化为CF时,并且对象的所有者发生改变,则使用CFBridgingRetain()__bridge_retained

当一个类型转化到另一种类型时,但是对象所有者没有发生改变,则使用__bridge.

6、 使用ARC注意事项

  • 属性命名不能用new开头
  • 不再使用retainrelaseautorelease
  • strong,weak,assign,copy,__weak__strong__autorelease@autorelease{}等的使用需要学习一下(特别注意一下__weak, __strong, __autorelease应该写在指针后边,变量名前面,否则不是正确写法,只是编译器会做一些处理)。

  • 重写dealloc方法不调用[super dealloc]方法。

  • 第三方库不支持arc,要將每个相关文件设置-fno-objc-arc。

  • arc对core foundation无效,需要自己控制内存,包括释放,并且需要cast的时候要用 __bridge__bridge_retain__bridge_transfer等修饰来控制对应内存。

  • arc和block的时候需要注意循环引用的问题。

  • 还有和C混用的时候需要注意,先將对象赋nil再free掉相关内存。避免使用 C 的 memcpy 和 realloc 函数等等。

  • 在dealloc中把成员变量置nil

7、 总结

ARC相对于MRC时弱引用时运行效率确实会慢一些,但他在[[[XXX alloc] init] autorelease]时ARC不但可以简化其写法,还可以让它更快的处理,减少不必要的入池操作,而且他的优势也是十分明显的。ARC只是为了提高您的工作效率,而不是一个神奇而没有缺点的一项技术。





Comments