iOS开发ARC内存管理技术要点
2016-02-19 00:48
381 查看
本文来源于我个人的ARC学习笔记,旨在通过简明扼要的方式总结出iOS开发中ARC(Automatic Reference Counting,自动引用计数)内存管理技术的要点,所以不会涉及全部细节。这篇文章不是一篇标准的ARC使用教程,并假定读者已经对ARC有了一定了解和使用经验。详细的关于ARC的信息请参见苹果的官方文档与网上的其他教程:)
本文的主要内容:
ARC的本质
ARC的开启与关闭
ARC的修饰符
ARC与Block
ARC与Toll-Free Bridging
Automatic Reference Counting (ARC) is a compiler-level feature that simplifies the process of managing object lifetimes (memory management) in Cocoa applications.
ARC只是相对于MRC(Manual Reference Counting或称为非ARC,下文中我们会一直使用MRC来指代非ARC的管理方式)的一次改进,但它和之前的技术本质上没有区别。具体信息可以参考ARC编译器官方文档。
如果需要对特定文件开启或关闭ARC,可以在工程选项中选择Targets -> Compile Phases -> Compile Sources,在里面找到对应文件,添加flag:
打开ARC:-fobjc-arc
关闭ARC:-fno-objc-arc
如图:
注意:如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要将强引用置nil。
一个常见的误解是,在ARC中没有autorelease,因为这样一个“自动释放”看起来好像有点多余。这个误解可能源自于将ARC的“自动”和autorelease“自动”的混淆。其实你只要看一下每个iOS App的main.m文件就能知道,autorelease不仅好好的存在着,并且变得更fashion了:不需要再手工被创建,也不需要再显式得调用[drain]方法释放内存池。
以下两行代码的意义是相同的。
这里关于autoreleasepool就不做展开了,详细地信息可以参考官方文档或者其他文章。
__autoreleasing在ARC中主要用在参数传递返回值(out-parameters)和引用传递参数(pass-by-reference)的情况下。
比如常用的NSError的使用:
(在上面的writeToFile方法中error参数的类型为(NSError *__autoreleasing *))
注意,如果你的error定义为了strong型,那么,编译器会帮你隐式地做如下事情,保证最终传入函数的参数依然是个__autoreleasing类型的引用。
所以为了提高效率,避免这种情况,我们一般在定义error的时候将其(老老实实地=。=)声明为__autoreleasing类型的:
在这里,加上__autoreleasing之后,相当于在MRC中对返回值error做了如下事情:
*error指向的对象在创建出来后,被放入到了autoreleasing pool中,等待使用结束后的自动释放,函数外error的使用者并不需要关心*error指向对象的释放。
另外一点,在ARC中,所有这种指针的指针 (NSError **)的函数参数如果不加修饰符,编译器会默认将他们认定为__autoreleasing类型。
比如下面的两段代码是等同的:
除非你显式得给value声明了__strong,否则value默认就是__autoreleasing的。
最后一点,某些类的方法会隐式地使用自己的autorelease pool,在这种时候使用__autoreleasing类型要特别小心。
比如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:
会隐式地创建一个autorelease pool,上面代码实际类似于:
为了能够正常的使用*error,我们需要一个strong型的临时引用,在dict的枚举Block中是用这个临时引用,保证引用指向的对象不会在出了dict的枚举Block后被释放,正确的方式如下:
现在可以完全忽略掉这个修饰符了,因为iOS 4早已退出历史舞台很多年。
苹果的文档中明确地写道:
You should decorate variables correctly. When using qualifiers in an object variable declaration,
the correct format is:
按照这个说明,要定义一个weak型的NSString引用,它的写法应该是:
而不应该是:
我相信很多人都和我一样,从开始用ARC就一直用上面那种错误的写法。
那这里就有疑问了,既然文档说是错误的,为啥编译器不报错呢?文档又解释道:
Other variants are technically incorrect but are “forgiven” by the compiler. To understand the issue, seehttp://cdecl.org/.
好吧,看来是苹果爸爸(=。=)考虑到很多人会用错,所以在编译器这边贴心地帮我们忽略并处理掉了这个错误:)虽然不报错,但是我们还是应该按照正确的方式去使用这些修饰符,如果你以前也常常用错误的写法,那看到这里记得以后不要这么写了,哪天编译器怒了,再不支持错误的写法,就要郁闷了。
在ARC中,以下代码会输出null而不是crash:)
这件事情在下面代码展示的情况中要更加额外小心。
在这段代码中,myController的completionHandler调用了myController的方法[dismissViewController...],这时completionHandler会对myController做retain操作。而我们知道,myController对completionHandler也至少有一个retain(一般准确讲是copy),这时就出现了在内存管理中最糟糕的情况:循环引用!简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。我们在delegate关系中,对delegate指针用weak就是为了避免这种问题。
不过好在,编译器会及时地给我们一个警告,提醒我们可能会发生这类型的问题:
本文的主要内容:
ARC的本质
ARC的开启与关闭
ARC的修饰符
ARC与Block
ARC与Toll-Free Bridging
ARC的本质
ARC是编译器(时)特性,而不是运行时特性,更不是垃圾回收器(GC)。Automatic Reference Counting (ARC) is a compiler-level feature that simplifies the process of managing object lifetimes (memory management) in Cocoa applications.
ARC只是相对于MRC(Manual Reference Counting或称为非ARC,下文中我们会一直使用MRC来指代非ARC的管理方式)的一次改进,但它和之前的技术本质上没有区别。具体信息可以参考ARC编译器官方文档。
ARC的开启与关闭
不同于XCode4可以在创建工程时选择关闭ARC,XCode5在创建的工程是默认开启ARC,没有可以关闭ARC的选项。如果需要对特定文件开启或关闭ARC,可以在工程选项中选择Targets -> Compile Phases -> Compile Sources,在里面找到对应文件,添加flag:
打开ARC:-fobjc-arc
关闭ARC:-fno-objc-arc
如图:
ARC的修饰符
ARC主要提供了4种修饰符,他们分别是:__strong,__weak,__autoreleasing,__unsafe_unretained。__strong
表示引用为强引用。对应在定义property时的"strong"。所有对象只有当没有任何一个强引用指向时,才会被释放。注意:如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要将强引用置nil。
__weak
表示引用为弱引用。对应在定义property时用的"weak"。弱引用不会影响对象的释放,即只要对象没有任何强引用指向,即使有100个弱引用对象指向也没用,该对象依然会被释放。不过好在,对象在被释放的同时,指向它的弱引用会自动被置nil,这个技术叫zeroing weak pointer。这样有效得防止无效指针、野指针的产生。__weak一般用在delegate关系中防止循环引用或者用来修饰指向由Interface Builder编辑与生成的UI控件。__autoreleasing
表示在autorelease pool中自动释放对象的引用,和MRC时代autorelease的用法相同。定义property时不能使用这个修饰符,任何一个对象的property都不应该是autorelease型的。一个常见的误解是,在ARC中没有autorelease,因为这样一个“自动释放”看起来好像有点多余。这个误解可能源自于将ARC的“自动”和autorelease“自动”的混淆。其实你只要看一下每个iOS App的main.m文件就能知道,autorelease不仅好好的存在着,并且变得更fashion了:不需要再手工被创建,也不需要再显式得调用[drain]方法释放内存池。
以下两行代码的意义是相同的。
initWithFormat:@"hehe"] autorelease]; // MRC NSString *__autoreleasing str = [[NSString alloc] initWithFormat:@"hehe"]; // ARC
这里关于autoreleasepool就不做展开了,详细地信息可以参考官方文档或者其他文章。
__autoreleasing在ARC中主要用在参数传递返回值(out-parameters)和引用传递参数(pass-by-reference)的情况下。
__autoreleasingis used to denote arguments that are passed by reference ([code]id *) and are autoreleased on return.
比如常用的NSError的使用:
NSError *__autoreleasing error; ? (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) ?{ NSLog(, error); }
(在上面的writeToFile方法中error参数的类型为(NSError *__autoreleasing *))
注意,如果你的error定义为了strong型,那么,编译器会帮你隐式地做如下事情,保证最终传入函数的参数依然是个__autoreleasing类型的引用。
NSError *error; NSError *__autoreleasing tempError = error; // 编译器添加 if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError]) ?{ error = tempError; // 编译器添加 NSLog(@"Error: %@", error); }
所以为了提高效率,避免这种情况,我们一般在定义error的时候将其(老老实实地=。=)声明为__autoreleasing类型的:
NSError *__autoreleasing error;
在这里,加上__autoreleasing之后,相当于在MRC中对返回值error做了如下事情:
*error = [[[NSError alloc] init] autorelease];
*error指向的对象在创建出来后,被放入到了autoreleasing pool中,等待使用结束后的自动释放,函数外error的使用者并不需要关心*error指向对象的释放。
另外一点,在ARC中,所有这种指针的指针 (NSError **)的函数参数如果不加修饰符,编译器会默认将他们认定为__autoreleasing类型。
比如下面的两段代码是等同的:
- (NSString *)doSomething:(NSNumber **)value { // do something }
- (NSString *)doSomething:(NSNumber * __autoreleasing *)value { // do something }
除非你显式得给value声明了__strong,否则value默认就是__autoreleasing的。
最后一点,某些类的方法会隐式地使用自己的autorelease pool,在这种时候使用__autoreleasing类型要特别小心。
比如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:
- (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,上面代码实际类似于:
- (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后被释放,正确的方式如下:
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error { __block NSError* 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; } ? }
__unsafe_unretained
ARC是在iOS 5引入的,而这个修饰符主要是为了在ARC刚发布时兼容iOS 4以及版本更低的设备,因为这些版本的设备没有weak pointer system,简单的理解这个系统就是我们上面讲weak时提到的,能够在weak引用指向对象被释放后,把引用值自动设为nil的系统。这个修饰符在定义property时对应的是"unsafe_unretained",实际可以将它理解为MRC时代的assign:纯粹只是将引用指向对象,没有任何额外的操作,在指向对象被释放时依然原原本本地指向原来被释放的对象(所在的内存区域)。所以非常不安全。现在可以完全忽略掉这个修饰符了,因为iOS 4早已退出历史舞台很多年。
*使用修饰符的正确姿势(方式=。=)
这可能是很多人都不知道的一个问题,包括之前的我,但却是一个特别要注意的问题。苹果的文档中明确地写道:
You should decorate variables correctly. When using qualifiers in an object variable declaration,
the correct format is:
ClassName * qualifier variableName;
按照这个说明,要定义一个weak型的NSString引用,它的写法应该是:
NSString * __weak str = @"hehe"; // 正确!
而不应该是:
__weak NSString *str = @"hehe"; // 错误!
我相信很多人都和我一样,从开始用ARC就一直用上面那种错误的写法。
那这里就有疑问了,既然文档说是错误的,为啥编译器不报错呢?文档又解释道:
Other variants are technically incorrect but are “forgiven” by the compiler. To understand the issue, seehttp://cdecl.org/.
好吧,看来是苹果爸爸(=。=)考虑到很多人会用错,所以在编译器这边贴心地帮我们忽略并处理掉了这个错误:)虽然不报错,但是我们还是应该按照正确的方式去使用这些修饰符,如果你以前也常常用错误的写法,那看到这里记得以后不要这么写了,哪天编译器怒了,再不支持错误的写法,就要郁闷了。
栈中指针默认值为nil
无论是被strong,weak还是autoreleasing修饰,声明在栈中的指针默认值都会是nil。所有这类型的指针不用再初始化的时候置nil了。虽然好习惯是最重要的,但是这个特性更加降低了“野指针”出现的可能性。在ARC中,以下代码会输出null而不是crash:)
- (void)myMethod { NSString *name; NSLog(@"name: %@", name); }
ARC与Block
在MRC时代,Block会隐式地对进入其作用域内的对象(或者说被Block捕获的指针指向的对象)加retain,来确保Block使用到该对象时,能够正确的访问。这件事情在下面代码展示的情况中要更加额外小心。
MyViewController *myController = [[MyViewController alloc] init…]; // 隐式地调用[myController retain];造成循环引用 myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; }; [self presentViewController:myController animated:YES completion:^{ [myController release]; // 注意,这里调用[myController release];是在MRC中的一个常规写法,并不能解决上面循环引用的问题 }];
在这段代码中,myController的completionHandler调用了myController的方法[dismissViewController...],这时completionHandler会对myController做retain操作。而我们知道,myController对completionHandler也至少有一个retain(一般准确讲是copy),这时就出现了在内存管理中最糟糕的情况:循环引用!简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。我们在delegate关系中,对delegate指针用weak就是为了避免这种问题。
不过好在,编译器会及时地给我们一个警告,提醒我们可能会发生这类型的问题:
相关文章推荐
- 网易新闻iOS版使用的18个开源组件
- IOS Socket 学习
- iOS开发探索-正则表达式
- ios图片格式转换
- iOS 归档 解档使用总结
- iOS之 isa 指针
- iOS 8 AutoLayout与Size Class自悟
- iOS 界面上绘制不同字体 颜色 大小的字符串
- iOS-全部开发证书无效
- nios中SDRAM时序约束的方法
- iOS开发——浅谈构架与用户体验
- ios闭包循环引用精讲
- 《招聘一个靠谱的 iOS》—参考答案(下)
- iOS 沙盒目录结构解析
- iOS学习----------支付宝咻咻咻
- ios-基础用法之【3】-@property、@synthesize和dynamic的用法
- iOS字典转json 包含字段nsstring
- iOS判断沙盒文件,计算文件总量,查看IOS沙盒中文件的属性(修改日期,创建日期,大小等)
- [iOS] 输入框高度随输入内容变化
- iOS 数据持久化 NSUserDefault 的使用 NSUserDefaults读取和写入自定义对象