初学者在学习Objective-c的时候,很容易在内存管理这一部分陷入混乱状态,很大一部分原因是没有弄清楚引用计数的原理,搞不明白对象的引用数量,这样就当然无法彻底释放对象的内存了,苹果官方文档在内存管理这一部分说的非常简单,只有三条准则:
如果在写代码的时候遵守这些准则,可以避免内存泄露,但是如果仅靠对这些准则的“记忆”来写代码的话,恐怕自己心里都不会有底,一旦遇到问题分析问题的时候很难从根本上找到问题出现的原因,本文分享了自己在理解引用计数时的分析过程,结合相关图形,希望能让大家深刻理解对象引用计数的原理。
当前对象的引用计数是多少呢?
为什么要提出这个问题,因为很多人会搞混对象的指针数量与引用数量的关系,不理解这个问题就弄不明白对象的引用计数到底为多少,当然就无法正确释放内存了。在说这个之前先简单了解一下堆内存与栈内存的概念,
变量名实际上是一个符号地址,在对程序编译连接时由系统给每一个变量名分配一个内存地址。在程序中从变量中取值,实际上是通过变量名找到相应的内存地址,从其存储单元中读取数据。指针是一个特殊的变量,因为它存放的是一个变量的地址。如下图所示:
上面这个内存模型相信大家都知道,指针与对象存在一个间接(指向)的关系,因此当指针指向一个对象的时候,很多人就会觉得这个指针引用到了该对象,进而就认为当指针指向一个对象的时候,该对象的引用计数就会加1,这种理解是一种感性的理解。实际上对于一个对象来说,它是不知道指向它的指针有多少个的,它的释放仅仅依靠的是引用计数,那么什么是引用计数呢?在objective-c中,大部分对象都继承于NSObject,NSObject包含一个用来保存引用数量的字段retainCount,说白了该字段就是引用计数,NSObject类的部分定义如下:
- (id)retain;
- (oneway void)release;
- (id)autorelease;
- (NSUInteger)retainCount;
- (NSString *)description;
因此,为了便于理解,我们可以把NSObject简化为如下模型:
对象能否释放就是判断其引用次数是否为零,也就是判断该对象的retainCount字段是否等于0,而指向该对象指针数量跟该对象retainCount字段的值并没有关系,因此指针数量并不等于引用数量,当指针指向该对象的时候,仅仅是给该指针变量赋值了,并没有修改对象的retainCount值,因此,指针指向一个对象的时候,该对象的引用计数是没有改变的。
以上面那段代码为例,我们调用Test对象的new方法的时候,会自动将retainCount的值设置为1,当我们将test1赋值给test2的时候,只是一个指针赋值,并没有修改对象的retainCount值,所以引用计数不变,依旧为1。
测试用例:
1 Engine *engine1 = [Engine new]; 2 NSLog(@"通过new消息创建对象engine1:"); 3 //输出engine1指针地址 4 NSLog(@"engine1 address is %p.",engine1); 5 //输出engine1的retainCount 6 NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]); 7 8 Engine *engine2 = engine1; 9 NSLog(@"将指针engine1复制给指针engine2:"); 10 //输出engine2指针地址 11 NSLog(@"engine2 address is %p.",engine2); 12 //输出engine1的retainCount 13 NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]); 14 //输出engine2的retainCount 15 NSLog(@"engine2 retainCount is %lu",(unsigned long)[engine2 retainCount]); 16 17 Engine *engine3 = [engine1 retain]; 18 NSLog(@"通过retain消息获得对象engine3:"); 19 //输出engine3指针地址 20 NSLog(@"engine3 address is %p.",engine3); 21 //输出engine1的retainCount 22 NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]); 23 //输出engine2的retainCount 24 NSLog(@"engine2 retainCount is %lu",(unsigned long)[engine2 retainCount]); 25 //输出engine3的retainCount 26 NSLog(@"engine3 retainCount is %lu",(unsigned long)[engine3 retainCount]); 27 28 [engine2 release]; 29 NSLog(@"给对象engine2发送消息release"); 30 NSLog(@"engine2 address is %p.",engine2); 31 NSLog(@"engine2 retainCount is %lu.",(unsigned long)[engine2 retainCount]);
输出结果如下:
从上面的输出结果可以得出以下几点结论:
因为这里需要输出引用计数,就没有采用ARC,所以会有一个小问题,那就是当退出局部环境的时候,即使局部指针所指向的对象已被销毁,局部指针变量的值仍然没有改变,因此需要手动赋值为nil。如果采用ARC的话,会自动回收内存并将指针赋值为nil。
不管是直接通过指针赋值还是通过retain或者copy来保留对象,都会增加指向对象的指针数量,这些指针都指向同一块内存地址,因为对象所分配的内存地址是不变的。
指向对象的指针的多少跟引用计数没有任何关系,但是通过retain、copy或release可以改变对象的引用计数。
仅当引用计数为0时才会释放对象占用的内存空间。
哎,真是“落花有意流水无情”啊,哪怕再多的指针“爱上对象”,人家这辈子却也只认引用计数。