引用计数:一个简单而有效的管理对象生命周期的方式,Objective-C和Swift的内存管理方式都是基于引用计数的。
引用计数的原理:当创建一个对象,它的引用计数为1,当有一个新的指针只想这个对象,它的引用计数+1,当某个指针不再指向这个对象时,我们将其引用计数-1,当对象的引用计数为0时,说明这个对象不再被任何指针指向了,这时候,就可以将这个对象销毁,回收内存。
NSObject *object = [[NSObject alloc]init]; //创建对象 引用计数 +1
NSLog(@"Reference Count = %u",[object retainCount]); // Reference Count = 1
[object release]; //引用计数 -1 对象会被释放掉。
为什么要使用引用计数?
- 当我们在一个函数内使用一个临时对象时,通常不需要修改它的引用计数,只需要在函数返回前将该对象销毁即可。
- 引用计数正真排上用场的是在面向对象的程序的设计架构中,用于对象之间传递和共享数据。
举个例子:
当对象A生成了一个对象M,需要调用对象B的一个方法,将对象M作为参数传给对象B。在没有引用计数的情况下,一般内存管理的原则是“谁申请谁释放”,那么对象A就需要在对B不需要对象M的时候讲M销毁,但对象B可能只是临时用一下对象M,也可能觉得对象M也很重要,讲它设置成自己的一个成员变量,在这种情况下,就不好确定什么时候该释放对象M。
- 方法一:暴力释放:就是在在A对象调用完对象B后,立马销毁对象M,然后B再复制一份对象M生成M2,然后自己管理M2的生命周期,但是这样做就会有一个很大的问题,内存申请、复制、释放的工作会耗费很多的性能。本来一个可以复用的对象,因为不方便管理其生命周期,就简单的把它销毁,又重新构造一份一模一样的,实在很影响性能。
- 方法二: 对象A在构造玩对象M后,始终不销毁对象M,由对象B去完成对象M的销毁工作,如果对象B需要长时间使用对象M就不销毁它。如果只是临时用一下,就可以用完立马销毁。虽然看似很好的解决了对象复制的问题,但是它强烈的依赖于A和B两个对象的配合,代码维护者需要明确的记住这种编程约定。而且,由于对象M的申请实在对象A中,释放在对象B中,是的它的内存管理代码非常的分散于不同的对象中,管理起来十分的费劲。如果这时候再复杂一点,对象B需要向C也传递对象M,那么这个时候对象C中又不能让对象C管理。所以这种方式带来的复杂性更大,更不可取。
所以引用计数就很好的解决了这个问题,哪些对象需要长时间的使用这个对象,就把它的引用计数+1,使用完之后-1,。所有的对象都遵循这个规则的话,对象的生命周期就可以完全交给引用计数了。我们也可以方便的享受对象带来的好处
不要向已经释放的对象发送消息
一个对象如果已经被释放回收,它所占的内容被复用了,那么就会造成程序异常崩溃。
NSObject *object = [[NSObject alloc]init]; //创建对象 引用计数 +1
NSLog(@"Reference Count = %u",[object retainCount]); // Reference Count = 1
[object release]; //引用计数 -1 对象会被释放掉。
NSLog(@"Reference Count = %u",[object retainCount]); // Reference Count = 1
上面这段代码中两次打印结果都是 Reference Count = 1,因为当系统最后一次执行release的时候,系统已经知道了你这个对象会被立马释放,所以没有必要再将retainCount 减 1 了,因为不管减不减 1 都会被释放回收,所以对应该对象的内存区域和retainCount的值也就变的毫无意义。不将这个值从1变成0,反而可以减少一次内存操作,加速对象的回收。对于内存,我们要锱铢必较。
循环引用
虽然引用计数这种管理内存的方式很简单,但是有一个较为大的问题就是,它不能解决循环引用。在block的使用中经常会遇到循环引用的问题。
举个例子:
- A、B连个对象,相互引用了对方作为自己的成员变量。只有自己销毁时,才将成员变量的引用计数减1.因为A的销毁依赖于B的销毁,同样的,B的销毁也依赖于A的销毁。这样就造成了死循环一样,即使外界已经没有人持有它们了,但是它们还是不能够销毁释放,这就是 循环引用。
- 不止两个对象会造成循环引用,多个对象也会造成循环引用,形成类似一个闭环,多个对象依次持有。在我们的实际开发中,当这个闭环越大的时候,就越难发现。
如何解决循环引用呢?
- 明确知道会在这里造成循环引用,根据具体业务逻辑在不需要的时候,合理的位置上主动断开闭环中的引用,使得对象回收。 这种方式需要程序员显示的手动去释放对象,相当于回到以前的“谁创建谁释放”的内存管理年代,它就需要程序员自己有能去发现循环引用,并且知道在什么时机断开循环引用回收内存。比较不常用。
-
弱引用:弱引用,虽然持有对象,但是不会增加引用计数,这样就避免了循环引用的问题。在iOS开发中,弱引用通常运用在delegate和block中。
在Xocede中自带有检测循环引用的工具。Xcode的工具集Instruments可以很方便的检测循环引用问题。
操作步骤:菜单栏 Product → Profile,程序运行完后选择 Leaks ,choose选择。
image.png
ARC
-
ARC(Automatic Reference Count 简称ARC)自动引用计数,用于内存管理的技术。ARC是WWDC2011大会提出来的技术,苹果将OSX上的垃圾回收机制飞起,采用ARC替代,现在的ARC技术已经很成熟了。笔者使用ARC的这几年,基本没有发生过内存泄漏问题。
-
ARC并不是GC(Garbage Collection 垃圾回收器),它只是一种代码静态分析(Static Analyzer)工具,背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,从而提高iOS开发人员的开发效率。
-
Apple的文档里是这么定义ARC的:“自动引用计数(ARC)是一个编译器级的功能,它能简化Cocoa应用中对象生命周期管理(内存管理)的流程。”
-
ARC的实现机制是在编译期完成的。在编译阶段,编译器将在项目代码中自动为分配对象插入retain、release和autorelease,且插入的代码不可见。
作用:
-.降低内存泄露等风险 ;
-.减少代码工作量,使开发者只需专注于业务逻辑
特别需要注意的
- ARC确实可以解决90%以上的内存管理问题,但是还是有10%需要开发者自己处理的,尤为要提的就是与底层 Core Foundation 对象交互的那部分,底层的 Core Foundation 对象不在ARC的管理下,所以需要手动处理这些对象的引用计数。
- 还有一个就是在过度使用block的时候,会出现循环引用的问题,但是又不能解决。
Core Foundation :
//创建一个CFStringRef对象
CFStringRef str = CFStringCreateWithCString(KCFAllocatorDefault, "Hello world",KCFStringEncodingUTF8);
//创建一个CTFontRef对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"AriaMT",fontSize,NULL);
//如果要对这些对象的引用计数做修改就要使用对应的 CFRetain 和 CFRelease 方法。
CFRetain(fonnRef) //引用计数 +1
CFRelease(fontRef) //引用计数 -1
当在ARC模式下使用 Core Foundation 时,我们需要将一个CF对象转成OC对象的时候,需要告诉编译器,转换过程中的引用计数如何调整。在转换时需要引入一些关键字:
-
__bridge
:只做类型转换,不修改相关的引用计数。原理的CF对象不在时,需要调用CFRelease。(反之亦然) -
__bridge_retained
:类型转换后,将相关对象引用计数加1,原来的CF对象不在时,需要调用CFRelease。 -
__bridge_transfer
:类型转换后,将该对象的引用计数交给ARC管理,CF对象不存在时,不需要调用CFRelease。
所以,我们需要根据具体的业务逻辑,合理使用上面这三种转换的关键字,就可以解决Core Foundation 与 Objective-C 对象相互转换的问题了。
网友评论