引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。不管是Objective-C语言还是Swift语言,其内存管理方式都是基于引用计数的。
1、引用计数
1.1 介绍
引用计数可以有效地管理对象生命周期:
- 创建新对象时,引用计数为1
- 有一个新指针指向这个对象时,引用计数加1
- 某个指针不再指向这个对象时,引用计数减1
-
对象引用计数变为0时,将对象销毁,回收内存
由于引用计数简单有效,除了Objective-C语言外,微软的COM(Component Object Model)、C++11(C++11提供了基于引用计数的智能指针share_prt)等语言也提供了基于引用计数的内存管理方式。为了运行示例,我们需要启动手工管理引用计数的模式,因为现在默认的工程都开启了自动的引用计数,文件属性加上-fno-objc-arc的编译参数:
image.png
image.png
当retainCount为0时,对象会被销毁,再向对象发送消息,程序会报错,因为这个时候指针指向的内存已经被销毁了,成为了野指针。
引用计数类似于Linux文件系统里面的硬链接: - 用ln命令可以创建一个硬链接(类似retain)
- 删除一个文件时,系统会检查文件的link count,大于1,不回收文件所占用的磁盘区域
- 最后一次删除,link count为1,系统才会执行真正的删除操作,把文件所占用的磁盘区域标记成未使用
1.2 引用计数的作用
1.1中的示例,反映不出引用计数真正的用处,因为示例中的对象的生命周期只是在一个函数中,而在函数内使用一个临时的对象,通常用不上它的引用计数,只要函数返回前将该对象销毁即可。应用计数真正排上用场的场景是在面向对象的程序设计架构中:用于对象直接传递和共享数据。
![](https://img.haomeiwen.com/i1723697/565c92a104dd58ed.png)
假如对象A生成了一个属性对象M,需要调用对象B的某一个方法,将对象M作为参数传递过去。对象M的释放原则如下:
- 对象A负责销毁M,使用M的对象B可能只是临时用一下对象M,也可能觉得对象M很重要,将它设置成自己的一个成员变量,那对象M的释放时机就成了一个难题!
- 暴力做法,对象A传递M后,立马销毁M,对象B需要复制一份M2,对象B自己管理M2的生命周期。但是,这样会带来更多的内存申请、复制、释放的工作。太影响性能。
- 对象B负责销毁M,对象B在用完后可以选择销毁M,这种做法强烈依赖于A、B两个对象的配合,代码维护者需要明确记住这种变成约定。对象M的申请和释放代码分散在不同的对象中,增加了管理的成本。如果遇到更复杂的情况,对象B又传递M给了对象C,实际的情况会更加复杂。
总结:
引用计数可以很好的解决上述问题,在参数M的传递过程中,哪些对象需要使用这个对象,就把它的引用计数+1,使用完了后引用计数-1。
所有对象都遵守这个规则的话,对象的生命周期管理就可以完全交给引用计数了。我们也可以很方便地享受到共享对象带来的好处。
1.3 释放对象
![](https://img.haomeiwen.com/i1723697/2b9c3d814c521852.png)
//引用计数为1时,再次释放
[object release];
//这时候引用计数为0,但是retainCount不会置0。在上图中,程序异常崩溃了
NSLog(@"Reference Count = %lu", (unsigned long)[object retainCount]);
当引用计数为0时,对象的内存会被回收,而我们向一个已经被回收的对象发了一个retainCount消息,它的输出结果应该是不确定的,如果该对象的所占的内存被复用了,那么就有可能造成程序异常崩溃。
当最后一次执行release,系统知道马上就要回收内存了,就没有必要将retainCount减少1了,因为不管减不减,该对象都肯定会被回收,它的所有的内存区域,包括retainCount值也变得没有意义。不将这个值从1变成0,可以减少一次内存的操作,加速对象的回收。
例如Linux文件系统举例,Linux文件系统下删除一个文件,也不是真正地在文件的磁盘区域进行抹除操作,而只是删除该文件的索引节点号。与引用计数的内存回收方式类似,即回收时只做标记,并不抹除相关的数据。
补充:
当对象的引用计数变成0的时候,系统会调用delloc方法去销毁对象。我们可以在delloc方法中去销毁之前引用对象的指针,以及取消已经订阅的KVO,通知等。
1、调用 -release :引用计数变为零
* 对象正在被销毁,生命周期即将结束.
* 不能再有新的 __weak 弱引用, 否则将指向 nil.
* 调用 [self dealloc]
2、父类 调用 -dealloc
* 继承关系中最底层的父类 在调用 -dealloc
* 如果是 MRC 代码 则会手动释放实例变量们(iVars)
* 继承关系中每一层的父类 都在调用 -dealloc
3、NSObject 调 -dealloc
* 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法
4、调用 object_dispose()
* 为 C++ 的实例变量们(iVars)调用 destructors
* 为 ARC 状态下的 实例变量们(iVars) 调用 -release
* 解除所有使用 runtime Associate方法关联的对象
* 解除所有 __weak 引用
* 调用 free()
1.4 循环引用及检测
引用计数这种管理内存的方式虽然很简单,但是有一个比较大的瑕疵,即它不能很好地解决循环引用问题。
![](https://img.haomeiwen.com/i1723697/1e42649a5e583c34.png)
当前对象A和对象B的引用计数都是1,如果想要将A和B销毁,必须置引用计数为0,在没有程序员介入的情况下,对象A要释放,其引用计数必须减一,这样其成员变量B的引用计数就会减一,但是对象A的引用计数要减一,必须让对象B销毁,即B的引用计数减一才可以,这样对象A和B都依赖于对方,都在等待对方去释放。
![](https://img.haomeiwen.com/i1723697/d661b814a4b4c553.png)
如上图所示,多个对象间,依次引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,即依次持有,形成一个环状,在真实的编程环境中,环越大就越难发现。
解决方案:
1、主动断开
主动断开循环引用依赖于程序员自己手工显示地控制,相当于回到“谁申请谁释放”的手动内存管理,同时也依赖于程序员的能力,发现循环引用并在合适的时机断开循环引用回收内存。
2、弱引用
弱引用,虽然持有对象,但并不增加引用计数。比如:delegate。
检测循环引用
Xcode的Instruments工具集:![](https://img.haomeiwen.com/i1723697/1f9fc1cda3c54290.png)
![](https://img.haomeiwen.com/i1723697/0d26b2f91863911d.png)
选择Leaks,然后点击Choose:
![](https://img.haomeiwen.com/i1723697/613942b39c0ec38c.png)
![](https://img.haomeiwen.com/i1723697/c429e09b2c64e47a.png)
上图中,已经检测到了一次内存泄漏,选中这次内存泄漏,下面详情区域切到Cycles & Roots:
![](https://img.haomeiwen.com/i1723697/9dc12b93fac78aeb.png)
使用Instrument显示设备离线(device offline)解决方案:
关闭Instruments,然后Xcode依次进行:清理(shift+command+K)+编译(command+B)+运行(command+I)
2、使用ARC
非ARC的应用,迁移到ARC,有一些迁移成本,但是Xcode专门集成了迁移工具,成本已经非常小了,而且,为了兼容第三方的非ARC开源库,你也可以在工程中随意使用编译参数-fno-objc-arc,这个参数允许对部分文件关闭ARC:
![](https://img.haomeiwen.com/i1723697/d25fbf3f48f8ecff.png)
虽然ARC是与IOS5一同推出的,但是由于ARC的实现机制是在编译期完成的,所以使用ARC之后应用仍然可以支持iOS4.3。稍微需要注意的是,如果要在ARC开启的情况下支持IOS4.3,需要将weak关键字换成__unsafe_unretained。
Core Foundation对象
CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, "hello world", kCFStringEncodingUTF8);
//CF对象需要手动管理
CFRetain(str);
CFRelease(str);
在ARC下,我们有时需要将一个Core Foundation对象转换成一个OC对象:
- ___bridge :只做类型转换,不修改相关对象的引用计数,原来的Core Foundation在不用时,需要调用CFRelease方法。
- ___bridge_retained :类型转换后,将对象的引用计数加+1,原来的Core Foundation对象在不用时,需要调用CFRelease方法。
- ___bridge_transfer :类型转换后,将对象的引用计数交给ARC管理,原来的Core Foundation对象在不用时,不需要调用CFRelease方法。
网友评论