前言
大家好,我是milo。iOS5.0开始,Apple有了ARC(Auto Reference Counting),ARC不同于MRC(Manual Reference Counting),它使得大部分类和自定义类不需要手动进行内存管理,它会在适当的时候回收内存,就像栈内存一样。但是作为一个ios开发者,我们需要通过MRC下的内存管理学习,加强对底层的理解。今天这篇文章讲的是ios内存管理的知识--引用计数。
堆和栈
我们知道内存是有堆和栈的,但是它们负责存储的东西不同。
栈负责存储非oc对象,也就是不继承NSOject的那些对象,和非对象类型如(int、char、float、double、struct、enum)等 ;
堆负责存储oc对象,也就是继承自NSObject的对象,包括其自带的许多类和包(UIKit等),以及自定义的类等等。
需要注意的是:栈内存是系统自动回收的,不需要程序员去管理,不存在内存泄漏的问题;堆内存是需要程序员区管理的,存在内存泄漏的问题,所以本篇文章“引用计数”是围着堆内存(前提是MRC)管理展开的。
对象的本质
在介绍下面的引用计数前,我们还需要了解对象的本质。
举个小小的例子说明:
JJPerson *person = [[JJPerson alloc] init];// 创建一个对象
上面这段代码是最常用的创建对象的方式,通过 [[JJPerson alloc] init] 创建出一个person对象。
但实际上,person这个东西,不是对象,它只是一个指向堆内存的指针。我们见下图:
等式的右边:JJPerson是一个继承NSObject的类,当它通过 [[JJPerson alloc] init] 创建一个对象的时候,就会在堆中开辟一段内存存储创建出来的JJPerson对象。(注:我在图中标注的地址是随意写的)
等式的左边: JJPerson *person 创建一个指针指向等式右边的内存。而在这以后就可以通过 person 这个指针自由的修改对象了。
引用计数
一、[xx retain] 和 [xx release]
每个OC对象都有自己的引用计数器,它是一个整数,从字面上, 可以理解为”对象被引用的次数”,也可以理解为: 它表示有多少人正在用这个对象。
那么上面的真实情况.png就变成了下面这样:
oc对象被创建出来的时候,他的引用计数为1,所以图中JJPerson对象的retainCount = 1。需要注意的是,当引用计数为0 时,当前的对象的堆内存就会被系统回收。
下面我们通过一些代码和代码的解释,来更加深入地理解引用计数。
#import
#import "JJPerson.h"
int main(int argc, const char * argv[]) {
// 创建一个对象,则对象的引用计数为1
JJPerson *person = [[JJPerson alloc] init];
NSLog(@"%zd",[person retainCount]);
return 0;
}
[person retainCount] 方法的作用是获取对象的引用次数,但是你会发现,编译器并没有提示你这个方法,而且会报错。这是因为iOS5.0以后默认都是开启ARC的,所以我们要做的是关闭ARC。
关闭以后我们的程序就可以正常编译通过了,打印如下:
2018-05-24 15:57:03.812508+0800 引用计数[963:61201] 1
刚好印证了刚才的“oc对象被创建出来的时候,他的引用计数为1”这句话。
接下来我们将调用 [person retain] 和 [person release] 两种方法,前一个方法的作用是让对象的引用计数+1,后一个方法的作用是让对象的引用计数-1。见如下代码:
(打印结果我就直接在 NSLog 后面以注释的形式给出)
#import
#import "JJPerson.h"
int main(int argc, const char * argv[]) {
// 创建一个对象,则对象的引用计数为1
JJPerson *person = [[JJPerson alloc] init];
NSLog(@"%zd",[person retainCount]);// 打印“1”
// 一次retain操作,引用计数+1
[person retain];
NSLog(@"%zd",[person retainCount]); // 打印“2”
// 一次release操作,引用计数-1
[person release];
NSLog(@"%zd",[person retainCount]);// 打印“1”
return 0;
}
小结:
1、创建一个对象,则对象的引用计数为1
2、一次retain操作,引用计数+1
3、一次release操作,引用计数-1
二、野指针 和 空指针
当我们将引用次数为1的对象再做一次release操作,再打印,会怎么样?
答:person指针所指的对象,也就是堆内存上的对象会被回收,此时的person称为野指针,调用任何方法会使程序崩溃。
我们接下去看代码:
#import
#import "JJPerson.h"
int main(int argc, const char * argv[]) {
// 创建一个对象,则对象的引用计数为1
JJPerson *person = [[JJPerson alloc] init];
NSLog(@"%zd",[person retainCount]);// 打印“1”
// 一次retain操作,引用计数+1
[person retain];
NSLog(@"%zd",[person retainCount]); // 打印“2”
// 一次release操作,引用计数-1
[person release];
NSLog(@"%zd",[person retainCount]);// 打印“1”
// 一次release操作,引用计数-1
[person release];
NSLog(@"%zd",[person retainCount]);// 打印“1”,为什么?
return 0;
}
为什么和我刚刚说的不一样?不仅没报错而且还打印“1”?
答: 默认情况下,Xcode是不会管僵尸对象(已经被销毁的对象)的,使用一块被释放的内存也不会报错。为了方便调试,应该开启僵尸对象监控。
然后程序就“正常的”如我们所料的报错了。
2018-05-24 16:09:28.674424+0800 引用计数[1100:75090] *** -[JJPerson test]: message sent to deallocated instance 0x1004128a0
编译器说不允许发送消息给已经释放的对象,所以说当野指针 person 还想使用[person retainCount] 这个方法的时候,程序就崩溃了。
既然野指针不行,那我们让 person = nil , 再调用方法,会不会崩溃?
答:不会。person = nil,那么person指针就称为空指针,空指针可以调用对象方法,但空指针调用方法时什么都不做,毕竟原来指向的那个对象完了当然就没办法干活了,而且也不会报错。(我们可以自己在 JJPerson 类里写一个测试方法,然后在main.m 上调用一下,结果就是前面我说的”什么都没发生“,在这里就不上代码了)
小结:
内存管理不当:
1、不再使用的对象未被回收,就会造成内存泄漏,程序会闪退
2、正在使用的对象被回收,会造成野指针,访问野指针会造成程序崩溃
3、空指针可以调用对象方法,但空指针调用方法时什么都不做,而且也不会报错
三、dealloc
当对象的 retainCount = 0 时,便会自动调用NSObject的方法 dealloc 回收堆内存,我们可以在 JJPerson类中重写这个方法,见如下代码:
#import "JJPerson.h"
@implementation JJPerson
-(void)dealloc {
NSLog(@"对象被释放了");
[super dealloc];
}
@end
是不是很奇怪 [super dealloc] 要放在最后,这是因为NSObject 的 dealloc 才是真正进行内存回收的代码,如果你一开始就调用了 [super dealloc] ,那就没有后面的什么事了,方法肯定不会往下走, NSLog(@"对象被释放了")
就别想打印出来了。
相似的道理,dealloc 这个方法应该避免主动调用,它是个回收对象的方法,你想想你一个对象调用它,是不是很矛盾,这样做也是防止带来许多的问题。
那么当我在 person 的 retainCount = 1 的时候,再次调用 [person release] ,出来的结果是:
2018-05-24 17:36:01.154622+0800 引用计数[1844:181200] 对象被释放了
说明系统已经自动调用了dealloc 方法回收内存了。
小结:
1、对象的引用计数为0 ,自动通过 dealloc 方法回收内存
2、[super dealloc] 才是真正回收内存的方法,必须在dealloc 方法的最后调用