Objective-C内存管理之引用计数

  初学者在学习Objective-c的时候,很容易在内存管理这一部分陷入混乱状态,很大一部分缘由是没有弄清楚引用计数的原理,搞不明白对象的引用数量,这样就固然没法完全释放对象的内存了,苹果官方文档在内存管理这一部分说的很是简单,只有三条准则:程序员

  1. 当你使用new、alloc或copy方法建立一个对象时,该对象的保留指针为1,当再也不使用该对象的时候,你应该想该对象发送一条release或autorelease消息,这样,该对象在其寿命结束时将被销毁。
  2. 当你经过其余方法得到一个对象时,假设该对象的保留计数器为1,并且已经设置为自动释放,那么你不须要执行任何操做来确保该对象被销毁。若是你打算在一段时间内拥有该对象,则须要保留它并确保它在操做完成时释放它。
  3. 若是你保留了某个对象,就须要(最终)释放或自动释放该对象。必须保持retain方法和release方法的使用次数相同。

  若是在写代码的时候遵照这些准则,能够避免内存泄露,可是若是仅靠对这些准则的“记忆”来写代码的话,恐怕本身内心都不会有底,一旦遇到问题分析问题的时候很难从根本上找到问题出现的缘由,本文分享了本身在理解引用计数时的分析过程,结合相关图形,但愿能让你们深入理解对象引用计数的原理。objective-c

遇到了问题?分析而后测试

  当前对象的引用计数是多少呢?数据结构

     为何要提出这个问题,由于不少人会搞混对象的指针数量与引用数量的关系,不理解这个问题就弄不明白对象的引用计数到底为多少,固然就没法正确释放内存了。在说这个以前先简单了解一下堆内存与栈内存的概念,学习

  1. 栈内存:由编译器负责分配,存放局部环境中定义的基本变量的值,例如方法中的基本变量等,离开局部环境时会由编译器自动释放其内存空间。
  2. 堆内存: 通常由程序员经过new或alloc等来手动分配,使用完后也须要程序员手动释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事。

     变量名其实是一个符号地址,在对程序编译链接时由系统给每个变量名分配一个内存地址。在程序中从变量中取值,其实是经过变量名找到相应的内存地址,从其存储单元中读取数据。指针是一个特殊的变量,由于它存放的是一个变量的地址。以下图所示:测试

 

  上面这个内存模型相信你们都知道,指针与对象存在一个间接(指向)的关系,所以当指针指向一个对象的时候,不少人就会以为这个指针引用到了该对象,进而就认为当指针指向一个对象的时候,该对象的引用计数就会加1,这种理解是一种感性的理解。实际上对于一个对象来讲,它是不知道指向它的指针有多少个的,它的释放仅仅依靠的是引用计数,那么什么是引用计数呢?在objective-c中,大部分对象都继承于NSObject,NSObject包含一个用来保存引用数量的字段retainCount,说白了该字段就是引用计数,NSObject类的部分定义以下:
- (id)retain;
- (oneway void)release;
- (id)autorelease;
- (NSUInteger)retainCount;
- (NSString *)description;

所以,为了便于理解,咱们能够把NSObject简化为以下模型:spa

 

  对象可否释放就是判断其引用次数是否为零,也就是判断该对象的retainCount字段是否等于0,而指向该对象指针数量跟该对象retainCount字段的值并无关系,所以指针数量并不等于引用数量,当指针指向该对象的时候,仅仅是给该指针变量赋值了,并无修改对象的retainCount值,所以,指针指向一个对象的时候,该对象的引用计数是没有改变的。3d

  以上面那段代码为例,咱们调用Test对象的new方法的时候,会自动将retainCount的值设置为1,当咱们将test1赋值给test2的时候,只是一个指针赋值,并无修改对象的retainCount值,因此引用计数不变,依旧为1。指针

测试用例:code

 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]);

输出结果以下:对象

  从上面的输出结果能够得出如下几点结论:

  1. 和本文一开始分析得出的结果同样,经过指针赋值并不能改变对象的引用计数。
  2. 不管是经过指针赋值仍是经过retain得到对象,它们都指向同一个内存地址,即:指向同一个对象
  3. 在对象的引用计数归零以前,全部指向它的指针都是可用的。经过某个指针发送release消息仅仅是让引用计数减一,该指针自己不会被销毁。

  由于这里须要输出引用计数,就没有采用ARC,因此会有一个小问题,那就是当退出局部环境的时候,即便局部指针所指向的对象已被销毁,局部指针变量的值仍然没有改变,所以须要手动赋值为nil。若是采用ARC的话,会自动回收内存并将指针赋值为nil。

总结

  不论是直接经过指针赋值仍是经过retain或者copy来保留对象,都会增长指向对象的指针数量,这些指针都指向同一块内存地址,由于对象所分配的内存地址是不变的。

  指向对象的指针的多少跟引用计数没有任何关系,可是经过retain、copy或release能够改变对象的引用计数。

  仅当引用计数为0时才会释放对象占用的内存空间。  

  哎,真是“落花有意流水无情”啊,哪怕再多的指针“爱上对象”,人家这辈子却也只认引用计数。

相关文章
相关标签/搜索