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

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

  如果在写代码的时候遵守这些准则,可以避免内存泄露,但是如果仅靠对这些准则的“记忆”来写代码的话,恐怕自己心里都不会有底,一旦遇到问题分析问题的时候很难从根本上找到问题出现的原因,本文分享了自己在理解引用计数时的分析过程,结合相关图形,希望能让大家深刻理解对象引用计数的原理。

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

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

为什么要提出这个问题,因为很多人会搞混对象的指针数量与引用数量的关系,不理解这个问题就弄不明白对象的引用计数到底为多少,当然就无法正确释放内存了。在说这个之前先简单了解一下堆内存与栈内存的概念,

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

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

  上面这个内存模型相信大家都知道,指针与对象存在一个间接(指向)的关系,因此当指针指向一个对象的时候,很多人就会觉得这个指针引用到了该对象,进而就认为当指针指向一个对象的时候,该对象的引用计数就会加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。

测试用例:

         Engine *engine1 = [Engine new];
NSLog(@"通过new消息创建对象engine1:");
//输出engine1指针地址
NSLog(@"engine1 address is %p.",engine1);
//输出engine1的retainCount
NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]); Engine *engine2 = engine1;
NSLog(@"将指针engine1复制给指针engine2:");
//输出engine2指针地址
NSLog(@"engine2 address is %p.",engine2);
//输出engine1的retainCount
NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]);
//输出engine2的retainCount
NSLog(@"engine2 retainCount is %lu",(unsigned long)[engine2 retainCount]); Engine *engine3 = [engine1 retain];
NSLog(@"通过retain消息获得对象engine3:");
//输出engine3指针地址
NSLog(@"engine3 address is %p.",engine3);
//输出engine1的retainCount
NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]);
//输出engine2的retainCount
NSLog(@"engine2 retainCount is %lu",(unsigned long)[engine2 retainCount]);
//输出engine3的retainCount
NSLog(@"engine3 retainCount is %lu",(unsigned long)[engine3 retainCount]); [engine2 release];
NSLog(@"给对象engine2发送消息release");
NSLog(@"engine2 address is %p.",engine2);
NSLog(@"engine2 retainCount is %lu.",(unsigned long)[engine2 retainCount]);

输出结果如下:

  从上面的输出结果可以得出以下几点结论:

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

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

总结

  不管是直接通过指针赋值还是通过retain或者copy来保留对象,都会增加指向对象的指针数量,这些指针都指向同一块内存地址,因为对象所分配的内存地址是不变的。

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

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

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

Objective-C内存管理之引用计数的更多相关文章

  1. iOS的内存管理和引用计数规则、Block的用法以及三种形式(stack、malloc、global)

    学习内容 iOS的内存管理和引用计数规则 内存管理的思考方式 自己生成的对象自己持有 非自己生成的对象自己也能持有 自己持有的对象不需要时释放 非自己持有的对象不能释放 ARC有效时,id类型和对象类 ...

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

    本文会继续深入学习OC内存管理,内容主要参考iOS高级编程,Objective-C基础教程,疯狂iOS讲义,是我学习内存管理的笔记 内存管理 1 内存管理的基本概念 1.1 Objective-C中的 ...

  3. Swift基础语法-内存管理, 自动引用计数

    1. 工作机制 Swift和OC一样,采用自动引用计数来管理内存 当有一个强引用指向某一个对象时,该对象的引用计数会自动+1 当该强引用消失时,引用计数会自动-1 当引用计数为0时,该对象会被销毁 2 ...

  4. Python内存管理及引用计数

    作为一门动态语言,python很重要的一个概念就是动态类型,即对象的类型和内存占用都是运行时确定的.(Why?)运行时,解释器会根据语法和右操作数来决定新对象的类型.动态类型的实现,是通过引用和对象的 ...

  5. Objective C内存管理之理解autorelease------面试题

    Objective C内存管理之理解autorelease   Autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该Object放入了当前的A ...

  6. Object-C内存管理-对象引用计数的特例

    看到OC中内存管理这块,其中的引用计数部分,部分10.5上的EBOOK示例已经在10.9上不能运行正确了,比如下面的代码: NSString * str1 = @"string 1" ...

  7. Objective C 内存管理[转]

    1  配对原则 alloc – release new – release retain - release copy – release 2  new和alloc-init的区别 (1)区别只在于a ...

  8. objective C 内存管理及属性方法具体解释

    oc为每一个对象提供一个内部计数器.这个计数器跟踪对象的引用计数,当对象被创建或拷贝时.引用计数为1.每次保持对象时,调用retain接口.引用计数加1.假设不需要这个对象时调用release,引用计 ...

  9. OC语法6——内存管理之引用计数器(retain,release)

    OC内存管理: 一.引用计数器: Java有垃圾回收机制(Garbage Collection,GC).也就是说当我们创建对象后,不需要考虑回收内存的事,Java的垃圾回收机制会自动销毁该对象,回收它 ...

随机推荐

  1. Gradle 实现 Android 多渠道定制化打包

    Gradle 实现 Android 多渠道定制化打包 版权声明:本文为博主原创文章,未经博主允许不得转载. 最近在项目中遇到需要实现 Apk 多渠道.定制化打包, Google .百度查找了一些资料, ...

  2. 原生js+css3实现图片自动切换,图片轮播

    运用CSS3transition及opacity属性 制作图片轮播动画 自己这两天根据用js来控制触发CSS3中transition属性,从而写出来的以CSS3动画为基础,js控制过程的图片轮播 运用 ...

  3. ActionContext.getContext().getSession()

    ActionContext.getContext().getSession() 获取的是session,然后用put存入相应的值,只要在session有效状态下,这个值一直可用 ActionConte ...

  4. swift 中关于open ,public ,fileprivate,private ,internal,修饰的说明

    关于 swift 中的open ,public ,fileprivate,private, internal的区别 以下按照修饰关键字的访问约束范围 从约束的限定范围大到小的排序进行说明 open,p ...

  5. Log4Net应用问题

    问题 一.日志存储方式 1.txt 2.SQLServer数据库 3.log文件 二.项目类型不同 1winFrom 2webFrom 3MVC 4WPF 5控制台 三.切分依据不同 1.空间大小 2 ...

  6. vim+vundle配置

    Linux环境下写代码虽然没有IDE,但通过给vim配置几个插件也足够好用.一般常用的插件主要包括几类,查找文件,查找符号的定义或者声明(函数,变量等)以及自动补全功能.一般流程都是下载需要的工具,然 ...

  7. 1 selenium3.0.1无法打开火狐浏览器

    [问题描述] 1.配置selenium3.0和java后,尝试打开火狐浏览器,提示缺少geckodriver驱动. [解决方案] 1.在http://www.seleniumhq.org/downlo ...

  8. 您真的理解了SQLSERVER的日志链了吗?

    您真的理解了SQLSERVER的日志链了吗? 先感谢宋沄剑给本人指点迷津,还有郭忠辉童鞋今天在QQ群里抛出的问题 这个问题跟宋沄剑讨论了三天,再次感谢宋沄剑 一直以来,SQLSERVER提供了一个非常 ...

  9. 一步步搭建自己的博客 .NET版(2、评论功能)

    前言 这次开发的博客主要功能或特点:    第一:可以兼容各终端,特别是手机端.    第二:到时会用到大量html5,炫啊.    第三:导入博客园的精华文章,并做分类.(不要封我)    第四:做 ...

  10. node应用线上部署时锁定包的依赖版本

    npm shrinkwrap 我们使用node开发时,经常需要依赖一些模块来完成功能需求,而我们所依赖的模块也必然会依赖其他模块,就这样一级一级的依赖,而且这些依赖模块并不为我们所控制.一个产品或项目 ...