一、简介

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,也可能将它设置为自己的一个成员变量,在这种情况下,什么时候销毁就成了一个难题了。

对于以上情况有两种做法:

  1. 对象 A 在调用完对象 B 的某个方法之后,马上销毁参数 O;然后对象 B 需要将对象 O 复制一份,生成另一个对象 O2,同时自己来管理对象 O2 的生命周期。

    这种做法带来更多的内存申请、复制、释放的工作。本来可以复用的对象,因为不方便管理它的生命周期,就简单地把它销毁,又重新构造一份一样的,实在太影响性能。

  2. 对象 A 只负责生成 O,之后就由对象 B 负责完成 O 的销毁工作。如果对象 B 只是临时用一下 O,就可以用完后马上销毁;如果对象 B 需要长时间使用 O,就不销毁它。

    这种做法看似解决了对象复制的问题,但是它强烈依赖于 A 和 B 两个对象的配合,代码维护者需要明确地记住这种编程约定。而且,由于 O 的生成和释放在不同对象中,使得它的内存管理代码分散在不同对象中,管理起来也很费劲。如果这个时候情况更加复杂一些,例如对象 B 需要再向对象 C 传递参数 O,那么这个对象在对象 C 中又不能让对象 C 管理。所以这种方法带来的复杂度更高,更加不可取。

引用计数的出现很好地解决这个问题,在参数 O 的传递过程中,哪些对象需要长时间使用它,就把它的引用计数 +1,使用完就-1。所有对象遵守这个规则,对象的生命周期管理就可以完全交给引用计数了。我们也可以很方便地享受到共享对象带来的好处。

六、ARC 下的内存管理问题

问题主要体现在:

  1. 过度使用 block 之后,无法解决循环引用问题。
  2. 遇到底层 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 拖拽生成的新变量是一致的。但是我个人认为这样做并不太合适。因为:

  1. 在创建这个对象时,需要注意临时使用一个强引用持有它,否则因为 weak 变量并不持有对象,就会造成一个对象刚被创建就销毁掉。

  2. 大部分 ViewController 的视图对象的生命周期与 ViewController 本身是一致的,没有必要额外做这个事情。

  3. 早先苹果这么设计,是有历史原因的。在早年,当时系统收到 Memory Warning 的时候,ViewController 的 View 会被 unLoad 掉。这个时候,使用 weak 的视图变量是有用的,可以保持这些内存被回收。但是这个设计已经被废弃了,替代方案是将相关视图的 CALayer 对应的 CABackingStore 类型的内存区会被标记成 volatile 类型,详见《再见,viewDidUnload方法》

6.5 检测循环引用

七、学习文章

iOS 内存管理
iOS 的内存管理

iOS 引用计数的更多相关文章

  1. iOS - 引用计数探讨

    <Objective-C 高级编程> 这本书有三个章节,我针对每一章节进行总结并加上适当的扩展分享给大家.可以从下面这张图来看一下这三篇的整体结构: 注意,这个结构并不和书中的结构一致,而 ...

  2. iOS开发--引用计数与ARC

    以下是关于内存管理的学习笔记:引用计数与ARC. iOS5以前自动引用计数(ARC)是在MacOS X 10.7与iOS 5中引入一项新技术,用于代替之前的手工引用计数MRC(Manual Refer ...

  3. iOS中引用计数内存管理机制分析

    在 iOS 中引用计数是内存的管理方式,虽然在 iOS5 版本中,已经支持了自动引用计数管理模式,但理解它的运行方式有助于我们了解程序的运行原理,有助于 debug 程序. 操作系统的内存管理分成堆和 ...

  4. 【iOS】自动引用计数 (循环引用)

    历史版本 ARC(Automatic Reference Counting,自动引用计数)极大地减少了Cocoa开发中的常见编程错误:retain跟release不匹配.ARC并不会消除对retain ...

  5. iOS内存管理机制解析之MRC手动引用计数机制

    前言: iOS的内存管理机制ARC和MRC是程序猿參加面试基本必问的问题,也是考察一个iOS基本功是 否扎实的关键,这样深入理解内存管理机制的重要性就不言而喻了. iOS内存管理机制发展史 iOS 5 ...

  6. iOS项目转移到自动引用计数

    这里主要参考了Apple官方文档:Transitioning to ARC Release Notes 在支持iOS5的Xcode4中,创建项目会看到这样的选项: 这是iOS5的新特性,自动对象引用计 ...

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

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

  8. iOS内存管理系列之一:对象所有权与引用计数

    当一个所有者(owner,其本身可以是任何一个Objective-C对象)做了以下某个动作时,它拥有对一个对象的所有权(ownership): 1. 创建一个对象.包括使用任何名称中包含“alloc” ...

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

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

随机推荐

  1. 7-12 产生每位数字相同的n位数 (30 分)

    读入2个正整数A和B,1<=A<=9, 1<=B<=10,产生数字AA...A,一共B个A 输入格式: 在一行中输入A和B. 输出格式: 在一行中输出整数AA...A,一共B个 ...

  2. node跨域方法

    第一种:jsonp 参看用nodejs实现json和jsonp服务 第二种:res.wirteHeadnode部分 var http = require('http') var url = requi ...

  3. uWSGI, send_file and Python 3.5

    当你的Flask项目通过Nginx+uWSGI成功部署的时候,当你很高兴你Flask里面的接口成功跑通的时候,你会发现真高兴!好牛逼! 然后当你写了其他几个接口的时候,在启动uWSGI服务的时候,死活 ...

  4. 最新IntelliJ IDEA 2019.3版本永久激活,一步到位!

    简单介绍一下什么是IDEA? IDEA全称 IntelliJ IDEA,是java编程语言开发的集成环境.IntelliJ在业界被公认为最好的java开发工具,尤其在智能代码助手.代码自动提示.重构. ...

  5. 调用系统的loading界面

    //在状态栏显示一个圈圈转动  代表正在请求 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

  6. [poj1062][最短路]昂贵的聘礼

    (最近总是有想让我的小博客更加充实的冲动,遇见一个不平常的题就想写下来.今天这个题姑且算是同学推荐的好题,很有意思,志之) 题目 题面 年轻的探险家来到了一个印第安部落里.在那里他和酋长的女儿相爱了, ...

  7. 修改webserver站点用户组权限

    例如webserver站点目录为webtest 搭建nginxwebserver服务器的时候,默认的用户和用户组权限为nginx:nginx, 即nginx.conf 和php-frm.conf 中默 ...

  8. AX中Json转化成表记录

    static void JsonToTable(str _json,Common _Common){    sysdictTable        dictTable;    TableId      ...

  9. 程序员过关斩将-- 喷一喷坑爹的面向UI编程

    摒弃面向UI编程 为何喷起此次话题,因为前不久和我们首席架构师沟通,谈起程序设计问题,一不小心把UI扯进来,更把那些按照UI来编程的后台工程师也扯了进来.今天特意百度了一下(其实程序员应该去googl ...

  10. MATLAB神经网络(1)之R练习

    )之R练习 将在MATLAB神经网络中学到的知识用R进行适当地重构,再写一遍,一方面可以加深理解和记忆,另一方面练习R,比较R和MATLAB的不同.如要在R中使用之前的数据,应首先在MATLAB中用w ...