一、内存管理
① 什么是内存管理?
- 当我们编写程序的时候,会声明各种各样的变量,编写各种各样的代码,它们都会占用内存,但是并不是所有的代码和内存都是由我们进行释放。
- 内存分为5个区域:栈、堆、bss段、数据段、代码段。
- 栈:存放的是局部变量,当局部变量的作用域结束的时候就会由系统进行释放局部变量所占用的内存空间;
- 堆:存放的是程序员手动申请的变量,手动申请的变量可以由程序员手动编写代码进行释放;
- bss段:存放的是为初始化的全局变量和静态变量,当全局变量和静态变量进行初始化的时候系统就会回收他们所占用的空间,然后把它们存放到数据段;
- 数据段:存放的是已经初始化的全局变量和静态变量和常量,当程序结束的时候由系统进行回收释放;
- 代码段:存放的是我们编写的代码,当程序结束的时候由系统进行回收释放;
- 因此,需要我们管理的其实只有堆空间。
- 其实除了内存区,还有内核区(系统用来进行内核处理操作的区域)和保留区(预留给系统处理 nil 等)。以 4GB 手机为例,系统将其中的 3GB 给了五大区+保留区,剩余的 1GB 给内核区使用,如下所示:
② 为什么要进行内存管理?
- iPhone 手机的内存是有限的,目前最大内存是 6G,当运行频繁多的app 的时候,会占用大量的内存。
- 当我们的 App 占用的内存大于 40M 的时候,iOS 系统会发出警告,当超过 45M 的时候系统会发出第二次警告,当占用内存超过 120M 的时候,App 会闪退,所以需要对内存空间进行一个合理的管理,用以保证 App 能够流畅的运行。
③ 内存管理的原则
- 当对象被创建出来以后,对象的引用计数默认是 1,所以在这个对象使用完毕以后,应该为这个对象发送一条 release 消息,保证这个对象在使用完毕以后应用计数变为零,并且占用的内存空间被回收;
- 当对象被别人使用的时候,别人就会为这个对象发送 retain 消息,表示使用的人多了一个,当别人不在使用对象的时候,别人就会为对象发送 release 消息,表示使用的人少了一个;
- 当对象还正在被使用的时候,对象就不应该被回收;
- 谁发送了 retain 消息,当使用完毕之后,谁要发送 release 消息;
- 当一个对象的引用计数为 0 时,系统就会销毁这个对象。
④ 引用计数是如何进行操作的?
- 我们可以向对象发送一些消息来操作对象的引用计数,发送 retain 消息可以添加一个引用计数,发送 release 消息可以减少一个引用计数;
- 当一个对象的应用计数为零的时候它会被立即回收。
二、内存管理方案
① MRC
- 在 MRC 时代,系统是通过对象的引用计数来判断一个是否销毁,有以下规则:
- 对象被创建时引用计数都为1;
- 当对象被其它指针引用时,需要手动调用[objc retain],使对象的引用计数+1;
- 当指针变量不再使用对象时,需要手动调用[objc release]来释放对象,使对象的引用计数-1;
- 当一个对象的引用计数为 0 时,系统就会销毁这个对象;
- 所以,在MRC模式下,必须遵守:谁创建,谁释放,谁引用,谁管理。
② ARC
- ARC 是 iOS 5 推出的新功能,全称叫 ARC(Automatic Reference Counting)。简单地说,就是代码中自动加入了 retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码,可以自动地由编译器完成了。
- 简单地理解ARC,就是通过指定的语法,让编译器(LLVM 3.0)在编译代码时,自动生成实例的引用计数管理部分代码。有一点,ARC 并不是 GC,它只是一种代码静态分析(Static Analyzer)工具。
- 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] 方法释放内存池。
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
NSString *str = [[[NSString alloc] initWithFormat:@"hello"] autorelease];
NSString *__autoreleasing str = [[NSString alloc] initWithFormat:@"hello"];
-
- __autoreleasing 在 ARC 中主要用在参数传递返回值(out-parameters)和引用传递参数(pass-by-reference)的情况下。比如常用的 NSError 的使用:
NSError *__autoreleasing error;
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) {
NSLog(@"Error: %@", error);
}
-
- 注意,如果 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 {
}
- (NSString *)doSomething:(NSNumber * __autoreleasing *)value {
}
- __unsafe_unretained
-
- 主要是针对 iOS 4,现在已经退出了,大家可以不需要深入了解。
③ ARC 与 MRC 性能对比
- 首先需要一个计时辅助函数,我这里选择使用 mach_absolute_time,计算时间差的函数如下:
double subtractTimes(uint64_t endTime, uint64_t startTime) {
uint64_t difference = endTime - startTime;
static double conversion = 0.0;
if(conversion == 0.0) {
mach_timebase_info_data_t info;
kern_return_t err = mach_timebase_info(&info);
if(err == 0)
conversion = 1e-9 * (double) info.numer / (double) info.denom;
}
return conversion * (double)difference;
}
- 然后定义两个测试类,一个是 ARC 环境下的,一个是 MRC 环境下的,分别如下:
+ (void)test {
uint64_t start,stop;
start = mach_absolute_time();
for (int i = 0; i < 1000000; i++) {
NSArray *array = [[NSArray alloc] init];
}
stop = mach_absolute_time();
double diff = subtractTimes(stop, start);
NSLog(@"ARC total time in seconds = %f
", diff);
}
+ (void)test {
uint64_t start,stop;
start = mach_absolute_time();
for (int i = 0; i < 1000000; i++) {
NSArray *array = [[NSArray alloc] init];
[array release];
}
stop = mach_absolute_time();
double diff = subtractTimes(stop, start);
NSLog(@"MRC total time in seconds = %f
", diff);
}
+ (void)test {
uint64_t start,stop;
start = mach_absolute_time();
for (int i = 0; i < 1000000; i++) {
NSArray *array = [[NSArray alloc] init];
}
stop = mach_absolute_time();
double diff = subtractTimes(stop, start);
NSLog(@"ARC total time in seconds = %f
", diff);
}
+ (void)test {
uint64_t start,stop;
start = mach_absolute_time();
for (int i = 0; i < 1000000; i++) {
NSArray *array = [[NSArray alloc] init];
[array release];
}
stop = mach_absolute_time();
double diff = subtractTimes(stop, start);
NSLog(@"MRC total time in seconds = %f
", diff);
}
ARC total time in seconds = 0.077761
MRC total time in seconds = 0.072469
ARC total time in seconds = 0.075722
MRC total time in seconds = 0.101671
- 从上面的数据可以看到,ARC 与 MRC 各有快慢的情况。即使上升到统计学的角度,ARC 也只是以轻微的优势胜出。看来我的测试姿势不对,并没有证明哪一方占绝对的优势。
- 那我们再来看看官方文档是怎么解释的?在Transitioning to ARC Release Notes中有这么一段话:
Is ARC slow?
It depends on what you’re measuring, but generally “no.” The compiler efficiently eliminates many extraneousretain/release calls and much effort has been invested in speeding up the Objective-C runtime in general. In particular, the common “return a retain/autoreleased object” pattern is much faster and does not actually put the object into the autorelease pool, when the caller of the method is ARC code.
One issue to be aware of is that the optimizer is not run in common debug configurations, so expect to see a lot more retain/release traffic at -O0 than at -Os.
- Steffen Itterheim 在 Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)一文中给出了大量的测试数据,Steffen Itterheim 通过他的测试得出一个结论:
ARC is generally faster, and ARC can indeed be slower
- Steffen Itterheim 指出大部分情况下,ARC 的性能是更好的,这主要得益于一些底层的优化以及 autorelease pool 的优化,这个从官方文档也能看到。但在一些情况下,ARC 确实是更慢,ARC 会发送一些额外的 retain/release 消息,如一些涉及到临时变量的地方,看下面这段代码:
{
id object = [array objectAtIndex:0];
[object doSomething];
[object doAnotherThing];
}
{
id object = [array objectAtIndex:0];
[object retain];
[object doSomething];
[object doAnotherThing];
[object release];
}
- (void) someMethod:(id)object {
[object doSomething];
[object doAnotherThing];
}
- (void) someMethod:(id)object {
[object retain];
[object doSomething];
[object doAnotherThing];
[object release];
}
- 这些些额外的 retain/release 操作也成了降低 ARC 环境下程序性能的罪魁祸首。但实际上,之所以添加这些额外的 retain/release 操作,是为了保证代码运行的正确性。如果只是在单线程中执行这些操作,可能确实没必要添加这些额外的操作。
- 但一旦涉及以多线程的操作,问题就来了,如上面的方法中,object 完全有可能在 doSoming 和 doAnotherThing 方法调用之间被释放。为了避免这种情况的发生,便在方法开始处添加了[object retain],而在方法结束后,添加了[object release]操作。