iOS 引用计数
一、简介
OC 在创建对象时,不会直接返回该对象,而是返回一个指向对象的指针。
OC 在内存管理上采用了引用计数,它是一个简单而有效管理对象生命周期的方式。在对象内部保存一个用来表示被引用次数的数字,init、new 和 copy 都会让计数 +1,调用 release 让计数 -1。当计数等于 0 的时候,系统调用 dealloc 方法来销毁对象。
A * a = [[A alloc] init]; // retain count = 1
A * b = a; // 指针赋值时,retain count 不会自动增加
[b retain]; // retain count = 2
{
OBJC_EXTERN int _objc_rootRetainCount(id);
NSObject * obj = [[NSObject alloc] init];
// 创建对象并引用,引用计数为 1
NSLog(@"obj retainCount:%lu", (unsigned long)_objc_rootRetainCount(obj));
NSObject * obj1 = [[NSObject alloc] init];
// 创建对象并引用,引用计数为 1
NSLog(@"obj1 retainCount:%lu", (unsigned long)_objc_rootRetainCount(obj1));
// obj 指向了 obj1 所指的对象 B,失去了对原来对象A的引用,所以对象A的引用计数-1,为 0。A 被销毁
// 对于 B,obj 引用了它,所以引用计数 +1,为 2
obj = obj1;
// self.obj 又引用了 A,所以引用计数 +1,为 3
self.obj = obj;
NSLog(@"strong obj1 retainCount:%lu",(unsigned long)_objc_rootRetainCount(obj1));
NSLog(@"strong obj retainCount:%lu",(unsigned long)_objc_rootRetainCount(obj));
}
引用计数分为自动引用计数「ARC : Automatic Reference Counting」和手动引用计数「MRC : Manual Reference Counting」。
二、原理
三、示例
NSObject * obj1 = [NSObject new];
NSLog(@"引用计数: %lu", (unsigned long)[obj1 retainCount]);
NSObject * obj2 = [obj1 retain];
NSObject * obj3 = [obj1 retain];
NSLog(@"引用计数: %lu", (unsigned long)[obj1 retainCount]);
[obj1 release];
NSLog(@"引用计数: %lu %@", (unsigned long)[obj1 retainCount], obj1);
[obj1 release];
NSLog(@"引用计数: %lu %@", (unsigned long)[obj1 retainCount], obj1);
[obj1 release];
NSLog(@"引用计数: %lu %@", (unsigned long)[obj1 retainCount], obj1);
引用计数:1
引用计数:3
引用计数:2 <NSObject:0x60400001ecd0>
引用计数:1 <NSObject:0x60400001ecd0>
*** -[NSObject retainCount]: message sent to deallocated instance 0x60400001ecd0
根据 Debug 输出可以看到:obj1 可以调用多次 release 方法。
从两次打印 obj1 的地址相同可以猜测,在 [obj1 release] 执行之后对象的引用计数 -1,不再强引用对象,但 obj1 仍然指向对象所在的那片内存空间。在第三次执行 release 后,对象的引用计数为 0,对象所在的内存空间被销毁,但是 obj1 指针仍然存在,此时调用 retainCount 会报野指针错误。可以通过置 obj1 = nil 解决这个问题。
对 Linux 文件系统比较了解的可能发现,引用计数的这种管理方式类似于文件系统里面的硬链接。在 Linux 文件系统中,我们用 ln 命令可以创建一个硬链接(相当于 retain),当删除一个文件时(相当于 release),系统调用会检查文件的 link count 值,如果大于 1,则不会回收文件所占用的磁盘区域。直到最后一次删除前,系统发现 link count 值为 1,则系统才会执行直正的删除操作,把文件所占用的磁盘区域标记成未用。
四、僵尸对象、野指针、空指针
僵尸对象:所占用内存已经被回收的对象,僵尸对象不能再使用。
野指针:指向僵尸对象(不可用内存)的指针,给野指针发送消息会报错(EXC_BAD_ACCESS)。
空指针:没有指向任何对象的指针(存储的是 nil、NULL),给空指针发送消息不会报错;空指针的一个经典使用场景就是在开发中获取服务器 API 数据时,转换野指针为空指针,避免发送消息报错。
五、为什么需要引用计数?
引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间传递和共享数据。
举个例子:
对象 A 生成了一个对象 O,需要调用对象 B 的某个方法,并将对象 O 作为参数传递过去。
[objB doSomething:O];
在没有引用计数的情况下,一般内存管理的原则是「谁申请谁释放」。
那么对象 A 就需要在对象 B 不再需要 O 的时候,将 O 销毁。但对象 B 可能临时用一下 O,也可能将它设置为自己的一个成员变量,在这种情况下,什么时候销毁就成了一个难题了。
对于以上情况有两种做法:
对象 A 在调用完对象 B 的某个方法之后,马上销毁参数 O;然后对象 B 需要将对象 O 复制一份,生成另一个对象 O2,同时自己来管理对象 O2 的生命周期。
这种做法带来更多的内存申请、复制、释放的工作。本来可以复用的对象,因为不方便管理它的生命周期,就简单地把它销毁,又重新构造一份一样的,实在太影响性能。
对象 A 只负责生成 O,之后就由对象 B 负责完成 O 的销毁工作。如果对象 B 只是临时用一下 O,就可以用完后马上销毁;如果对象 B 需要长时间使用 O,就不销毁它。
这种做法看似解决了对象复制的问题,但是它强烈依赖于 A 和 B 两个对象的配合,代码维护者需要明确地记住这种编程约定。而且,由于 O 的生成和释放在不同对象中,使得它的内存管理代码分散在不同对象中,管理起来也很费劲。如果这个时候情况更加复杂一些,例如对象 B 需要再向对象 C 传递参数 O,那么这个对象在对象 C 中又不能让对象 C 管理。所以这种方法带来的复杂度更高,更加不可取。
引用计数的出现很好地解决这个问题,在参数 O 的传递过程中,哪些对象需要长时间使用它,就把它的引用计数 +1,使用完就-1。所有对象遵守这个规则,对象的生命周期管理就可以完全交给引用计数了。我们也可以很方便地享受到共享对象带来的好处。
六、ARC 下的内存管理问题
问题主要体现在:
- 过度使用 block 之后,无法解决循环引用问题。
- 遇到底层 Core Foundation 对象,需要手工管理它们的引用计数时,显得一筹莫展。
6.1 循环引用
引用计数这种管理内存的方式虽然很简单,但是有一个比较大的瑕疵,即它不能很好的解决循环引用问题。如下图所示:对象 A和对象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁又依赖于对象 A 的销毁,这样就造成了循环引用 Reference Cycle 的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。
不止两对象存在循环引用问题,多个对象依次持有对方,形式一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现。下图是 4 个对象形成的循环引用问题。
6.2 主动断开循环引用
解决循环引用问题主要有两个办法。第一个办法:明确知道这里会存在循环引用,在合理的位置主动断开环中的一个引用,使得对象得以回收。如下图所示:
主动断开循环引用这种方式常见于各种与 block 相关的代码逻辑中。
不过,主动断开循环引用这种操作依赖于程序员自己手工显式地控制,相当于回到了以前 “谁申请谁释放” 的内存管理年代,它依赖于程序员自己有能力发现循环引用并且知道在什么时机断开循环引用回收内存,所以这种解决方法并不常用,更常见的办法是使用弱引用的办法。
6.3 使用弱引用
弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在 iOS 开发中,弱引用通常在 delegate 模式中使用。如下所示:
6.4 弱引用的实现原理
弱引用的实现原理是这样,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。
从这个原理中,我们可以看出,弱引用的使用是有额外的开销的。虽然这个开销很小,但是如果一个地方我们肯定它不需要弱引用的特性,就不应该盲目使用弱引用。举个例子,有人喜欢在手写界面的时候,将所有界面元素都设置成 weak 的,这某种程度上与Xcode 通过 Storyboard 拖拽生成的新变量是一致的。但是我个人认为这样做并不太合适。因为:
在创建这个对象时,需要注意临时使用一个强引用持有它,否则因为 weak 变量并不持有对象,就会造成一个对象刚被创建就销毁掉。
大部分 ViewController 的视图对象的生命周期与 ViewController 本身是一致的,没有必要额外做这个事情。
早先苹果这么设计,是有历史原因的。在早年,当时系统收到 Memory Warning 的时候,ViewController 的 View 会被 unLoad 掉。这个时候,使用 weak 的视图变量是有用的,可以保持这些内存被回收。但是这个设计已经被废弃了,替代方案是将相关视图的 CALayer 对应的 CABackingStore 类型的内存区会被标记成 volatile 类型,详见《再见,viewDidUnload方法》。
6.5 检测循环引用
七、学习文章
iOS 引用计数的更多相关文章
- iOS - 引用计数探讨
<Objective-C 高级编程> 这本书有三个章节,我针对每一章节进行总结并加上适当的扩展分享给大家.可以从下面这张图来看一下这三篇的整体结构: 注意,这个结构并不和书中的结构一致,而 ...
- iOS开发--引用计数与ARC
以下是关于内存管理的学习笔记:引用计数与ARC. iOS5以前自动引用计数(ARC)是在MacOS X 10.7与iOS 5中引入一项新技术,用于代替之前的手工引用计数MRC(Manual Refer ...
- iOS中引用计数内存管理机制分析
在 iOS 中引用计数是内存的管理方式,虽然在 iOS5 版本中,已经支持了自动引用计数管理模式,但理解它的运行方式有助于我们了解程序的运行原理,有助于 debug 程序. 操作系统的内存管理分成堆和 ...
- 【iOS】自动引用计数 (循环引用)
历史版本 ARC(Automatic Reference Counting,自动引用计数)极大地减少了Cocoa开发中的常见编程错误:retain跟release不匹配.ARC并不会消除对retain ...
- iOS内存管理机制解析之MRC手动引用计数机制
前言: iOS的内存管理机制ARC和MRC是程序猿參加面试基本必问的问题,也是考察一个iOS基本功是 否扎实的关键,这样深入理解内存管理机制的重要性就不言而喻了. iOS内存管理机制发展史 iOS 5 ...
- iOS项目转移到自动引用计数
这里主要参考了Apple官方文档:Transitioning to ARC Release Notes 在支持iOS5的Xcode4中,创建项目会看到这样的选项: 这是iOS5的新特性,自动对象引用计 ...
- iOS的内存管理和引用计数规则、Block的用法以及三种形式(stack、malloc、global)
学习内容 iOS的内存管理和引用计数规则 内存管理的思考方式 自己生成的对象自己持有 非自己生成的对象自己也能持有 自己持有的对象不需要时释放 非自己持有的对象不能释放 ARC有效时,id类型和对象类 ...
- iOS内存管理系列之一:对象所有权与引用计数
当一个所有者(owner,其本身可以是任何一个Objective-C对象)做了以下某个动作时,它拥有对一个对象的所有权(ownership): 1. 创建一个对象.包括使用任何名称中包含“alloc” ...
- Objective-C内存管理之-引用计数
本文会继续深入学习OC内存管理,内容主要参考iOS高级编程,Objective-C基础教程,疯狂iOS讲义,是我学习内存管理的笔记 内存管理 1 内存管理的基本概念 1.1 Objective-C中的 ...
随机推荐
- LeetCode---二叉树3-总结例题
二叉树-总结例题 1-从中序与后序遍历序列构造二叉树 给定二叉树的后序遍历和二叉树的中序遍历 想法: 先根据后序遍历的最后一个元素构造根节点 寻找根节点在中序遍历中的位置 递归构建根节点的左右子树 / ...
- Web环境从Apache转Nginx后页面报404错误
问题原因: Apache支持伪静态规则在项目的入口目录有个.htaccess文件,Apache默认识别此文件内容, 但是Nginx不识别.htaccess文件,导致伪静态规则失效,从而无法解析url地 ...
- JUC-八锁现象和不安全锁
1,被 synchronized 修饰的方法,锁的对象是方法的调用者(实例对象) 2,被 static 修饰的方法,锁的对象就是 Class模板对象,这个则全局唯一 问题7: 一个普通同步方法,一个静 ...
- Mac中使用brew安装mysql
若不考虑版本直接执行以下命令 brew install mysql 若要选择版本只要加上@版本即可,例如 brew install mysql@5.7 安装完后启动mysql mysql.server ...
- 分享一个快速审查js操作Dom的css
第一步 打开开发者工具第二步 打开 Sources 面板第三步 执行用户操作让对象可见(例如鼠标悬停)第四步 在元素可见的时候按下 F8(与“暂停脚本执行”按钮相同)第五步 点击开发者工具左上角的“选 ...
- IntelliJ IDEA神器使用技巧
说明:详情请参考慕课网课程:IntelliJ IDEA神器使用技巧:http://www.imooc.com/learn/924(感谢课程作者:闪电侠) 推荐: 1. 课程老师(闪电侠)IDEA快捷键 ...
- Vue的模板内换行问题
在用vue的模板{{}}进行渲染文本时候,字符串换行不起作用,后使用ES6的模板字符串进行换行仍然不起作用,解决方法: <div>{{str}}</div> 可换为用v-htm ...
- centOS 6.5 yum升级 gcc4.8 然后又退回来4.4
CentOS 6.5 用了很多年了,一直舍不得省7 . 由于要用到 c++ 11 ,所以决定升级一下. 为了省事我选择用 yum 方式升级,结果最后还是不能用,差点搞坏,这是真机,重装麻烦了. get ...
- TLS/SSL 梳理
数据加密通篇都是为了防止第三方的劫持伪造,保证连接安全, 毫无遮掩的明文传输只有民风淳朴的时候才是安全的. 先是一些基础的内容: 对称加密 最开始为了对数据进行加密,使用的是对称加密算法,即双方协商好 ...
- const 详解
简单分类: 常变量 const 类型 变量名 或者 类型 const 变量名 常引用 const 类型& 引用名 ...