iOS - 理解内存管理
iOS当中当我们提到内存管理首先想到的是引用计数,引用计数(Reference Count)是一个简单有效的管理对象生命周期的方式。不管是OC语言还是Swift语言,其内存管理方式都是基于引用计数的。如果你对这一块不是很清晰,一定要耐心的看看,下面就先说一下这种内存管理方式的特点及注意事项。
1.什么是引用计数,原理是什么?
引用计数可以有效的管理对象的生命周期。当我们创建一个新的对象的时候,它的引用计数为1,当有一个新的指针指向这个对象的时候,我们将其引用计数加1,当某个指针不再指向这个对象时,我们将其引用计数减1,当对象的引用计数变为0时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象摧毁,回首内存。
由于引用计数简单有效,出来oc语言外,微软的COM(Component Object Model)、C++11(C++11提供了引用计数的智能指针share_prt)等语言也提供了基于引用计数的内存管理方式。如下图:
为了更加形象,我们来看一段oc 的代码。新建一个工程,因为现在默认的工程开启了自动引用计数ARC,所以先设置一下,给AppDelegate.m加上-fon-objc-arc的编译参数,如下图:
然后,我们在输入下面这段代码,看一下log的结果:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject * object = [[NSObject alloc]init];
//创建对象,引用计数 = 1
NSLog(@"Reference Count = %lu",[object retainCount]);
NSObject * another = [object retain];
//another对象 持有 object对象,object引用计数+1 = 2
NSLog(@"Reference Count = %lu",[object retainCount]);
[another release];
//another对象 放弃持有发了release通知释放了 object对象,object引用计数-1 = 1
NSLog(@"Reference Count = %lu",[object retainCount]);
//object发送release通知释放对象
[object release];
//此时object的引用计数 = 0
//这里,object的内存被释放了
return YES;
}
打印的结果为:
2017-07-04 13:46:54.464 ReferenceCountText[25990:15056696] Reference Count = 1
2017-07-04 13:46:54.465 ReferenceCountText[25990:15056696] Reference Count = 2
2017-07-04 13:46:54.465 ReferenceCountText[25990:15056696] Reference Count = 1
看到这里大家应该明白什么是引用计数了吧。
2.为什么需要引用计数?
从上面那个简单的栗子中,我们还看不出引用计数的真正的用处。因为该对象的生命期只是在一个函数中,所以真实的应用场景中,我们在函数内使用一个临时对象,通常不需要修改它的引用计数的,只需要在函数返回前将对象销毁即可。
引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间的传递和共享数据。我们举个栗子看一下:
假如对象A生成了一个对象M,需要调用对象B的某一个方法,将对象M作为参数传递过去。在没有引用计数的情况下,一般内存管理的原则是“谁申请谁释放”,那么对象A就需要在对象B不再需要对象M的时候,将对象M摧毁掉。但对象B可能只是临时用一下对象M,也可能觉得对象M很重要,将它设置成自己的一个变量,在这种情况下,什么时候摧毁对象M就成为一个难题了,如下图:
这种情况,有一种很黄很暴力的做法,就是对象A在调用完对象B之后,马上销毁参数对象M,然后对象B需要将参数另外复制一份,生成另一个对象M2,然后自己管理对象M2的生命期。但是这种做法有一个很大的问题,就是它会带来更多的内存申请、复制、释放的工作。本来一个可以复用的对象,因为不方便管理它的生命期,就简单的把它摧毁,又重新构造一份一样,真的影响性能。请看下图:
我们还有另外一种方法,就是对象A在构造完对象M之后,始终不销毁对象M,由对象B来完成对象M的销毁工作。如果对象B需要长时间使用对象M,就不销毁它,如果只是临时用一下,就可以马上销毁。这种做法看似很好的解决了对象复制的问题,但是它强烈的依赖于A、B两个对象的配合,代码维护者需要明确的记住这种编程的约定。而且,由于对象M的申请是在对象A中,而释放在对象B中,使得它的内存管理代码分布在两个对象中,管理起来也是废了老逼劲了。
如果这个时候,情况在复杂一些,举个恶心的栗子,对象B需要再向对象C传递对象M,那么这个对象在对象C中又不能让对象C管理。所以这种方式带来的复杂性更加大,不可取。
所以,bb了这么多,就想说引用计数很好的解决了这个问题,在参数M传递的过程中,哪些对象需要长时间使用这个对象,就把它的引用计数加1,使用完了再把引用计数减1.所有的对象都遵循这个规则的话,对象的生命期的管理工作就完全交给了引用计数 了,成功甩锅。我们也可以很方便的享受到共享对象带来的快感和好处。
3.不要向已经释放的对象发送消息
有的萌萌哒的小伙伴想测试一下当对象释放的时候,其retainCount是否变成了0,他们的代码如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject * object = [[NSObject alloc]init];
NSLog(@"Reference Count = %lu",[object retainCount]);
[object release];
NSLog(@"Reference Count = %lu",[object retainCount]);
return YES;
}
如果真的这么做了,你得到的结果可能 是这样的:
ReferenceCountText[26104:15162772] Reference Count = 1
ReferenceCountText[26104:15162772] Reference Count = 1
为什么第二次的输出不是0 尼?咋回事儿,这是因为该对象的内存已经被回收了,而我们向一个已经回收的对像发了一个retainCount消息,所以它的输出结果应该是不确定的,如果该对象所占的内存被复用了,那么就可能直接崩。这就是我为什么说上面的结果是可能出现的。
那为什么在这个对象被回收之后,这个不确定的值是1而不是0呢,刨根问底拦不住,让我们好好刨一刨,这是因为当最后一次执行release时,系统知道马上就要回收内存了,就没有必要在将retainCount减1了,因为不管减不减1,该对象都肯定被释放,没跑了,肯定是得弄它。而在对象被回收后,它所有的内存区域,包括retainCount变的就是毫无意义。不将这个引用计数从1变0 ,可以减少一次内存操作,加速对象的回收。
4.循环引用(reference cycles)问题
今天累的一匹,请听下回分解。