iOS内存管理理论知识过一遍
目录
1】为什么要进行内存管理
2】内存管理的方式
3】自动引用计数技术(ARC)
一、为什么要进行内存管理
二、内存管理的方式
1、引用计数这套方案应用广泛,在多种语言中使用
引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。不管是OC语言还是Swift语言,其内存管理方式都是基于引用计数的。
引用计数可以有效地管理对象生命周期。当我们创建一个新对象的时候,它的引用计数为1,当有一个新的指针指向这个对象时,我们将其引用计数加1,当某个指针不再指向这个对象时,我们将其引用计数减1,当对象的引用计数变为0时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。
由于引用计数简单有效,除了OC语言外,微软的COM(Component Object Model)、C++11(C++11 提供了基于引用计数的只能指针 share_prt)等语言也提供了基于引用计数的内存管理方式。
对Linux文件系统比较了解的话会发现,引用计数的这种管理方式类似于文件系统里面的硬链接。在Linux文件系统中,我们用In命令可以创建一个硬链接(相当于我们这里的retain),当删除一个文件时(相当于我们这里的release),系统调用会检查文件的link count值,如果这个值大于1,则不会回收文件所占用的磁盘区域。直到最后一次删除前,系统会发现link count 值为1,系统才会执行真正的删除操作,把文件所占用的磁盘区域标记成“未使用”。
2、引用计数这套方案架构简单思路清晰
如果只是在一个函数的实现中使用一个临时的对象,通常是不需要修改它的引用计数的,只需要在函数返回前将该对象销毁就行了。引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间传递和共享数据。举一个例子:
假如对象A生成了一个对象M,对象A调用对象B的某一个方法,然后将对象M作为这个方法的参数传递过去。在没有引用计数的情况下,一般内存管理的原则是“谁申请谁释放”,那么对象A就需要在对象B不再需要对象M的时候,将对象M销毁。但对象B可能只是临时用一下对象M,也可能觉得对象M很重要,将它设置成自己的一个成员变量,在这种情况下,什么时候销毁对象M就成了一个难题。
对于这种情况,有一个暴力的做法,就是对象A在将对象M作为参数调用完对象B的方法之后,马上就销毁参数对象M,然后对象B需要将参数另外复制一份,生成另一个对象M2,然后自己管理对象M2的生命周期。但是这种做法有一个很大的问题,就是它带来了更多的内存申请、复制、释放的工作。本来一个可以复用的对象,因为不方便管理它的生命周期,就简单地把它销毁,又重新构造一份一样的,实在太影响性能。
还有另一种办法,就是对象A在构造完对象M之后,始终不销毁对象M,由对象B来完成对象M的销毁工作。如果对象B需要长时间使用对象M,就不销毁它,如果只是临时用一下,则可以用完后马上销毁。这种做法看似很好地解决了对象复制的问题,但是它强烈依赖于A,B两个对象的配合,代码维护者需要明确地记住这种编程约定,而且,由于对象M的申请是在对象A中,释放在对象B中,使得它的内存管理代码分散在不同对象中,管理起来也非常费劲。如果这个时候情况在复杂一些,例如对象B需要再向对象C传递对象M,那么这个对象在对象C中又不能让对象C管理。所以这种方式带来的复杂性更大,更不可取。
而引用计数这种方式能很好地解决这个问题,在参数M的传递过程中,哪些对象需要长时间使用这个对象,就把对象M的引用计数加1,使用完了之后再把对象M的引用计数减1。所有对象都遵循这个规则的话,对象的生命期管理就可以完全交给引用计数了。
NSObject协议声明了下面三个方法用于操作引用计数,以递增或者递减其值:
·retain:递增引用计数
·release:递减引用计数
·autorelease:待稍后清理“自动释放池”时,再递减引用计数
应用程序中会创建出很多的对象,彼此之间有引用关系,如果按照“引用树”回溯,那么最终会发现一个“根对象”,在MacOS系统中,此对象就是NSApplication对象;而在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时创建的单例。
3、使用引用计数的注意事项
3.1、不要向已经释放的对象发送消息
因为该对象的内存已经被回收了,而我们向一个已经被回收的对象发送消息,得到的结果是不确定的,如果该对象所占的内存被复用了,那么就有可能造成程序异常崩溃。
当对象被销毁后(它的引用计数减至0,并且通过dealloc方法),对象的引用变得失效。如果你有一个这样的引用,通常被称为悬挂指针的引用。给悬挂指针发送消息往往会出现意外,甚至应用发生崩溃。一些程序会给已经释放的对象发送release消息,主要是未追踪对象的retain和release。这样过度释放对象,会引起程序崩溃。
3.2、循环引用问题
引用计数这种管理内存的方式虽然简单,但是有一个比较大的瑕疵,就是它不能很好的解决循环引用问题。比如,对象A和对象B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1。但是又因为对象A的销毁依赖于对象B的销毁,而对象B的销毁又依赖于对象A的销毁,这样就造成了循环引用的问题,这种情况下,即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。
不止两个对象时会存在循环引用问题,多个对象依次持有,形成一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现这种循环引用问题。
循环引用问题是造成内存泄漏的主要原因,解决循环引用问题的主要两个办法,第一个办法是我明确知道这里会存在循环引用,在合理的位置主动断开环中的一个引用,使得对象得以回收,不过,“主动断开循环引用”这种操作依赖于程序员自己手工显式地控制,相当于回到了以前“谁申请谁释放”的内存管理年代,它需要程序员自己有能力发现循环引用,并且知道在什么时机断开循环引用回收内存(这通常与具体的业务逻辑相关),所以这种解决办法并不常用,更常见的是使用弱引用(weak reference)的办法。第二个办法,弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在iOS开发中,弱引用通常在delegate模式中使用。比如,控制器B成为控制器A的代理(delegate),控制器A的delegate成员变量通常是一个弱引用,以避免两个控制器相互引用造成循环引用问题。
虽然使用不好引用计数技术,容易造成循环引用,并且甚至还无法察觉,但是Xcode中有工具可以检测循环引用。
Xcode的Instrument工具集可以很方便地检测循环引用,在Xcode的菜单栏选择“product”-“Profile”,然后选择“Leaks”,再单击右下角的“Profile”按钮开始检测,这个时候iOS模拟器会运行起来,我们在模拟器里面进行正常的使用操作,如果Instrument检测到了循环引用,Instrument中会用一条红色的线条来表示一次内存泄漏的产生,切换到“Leaks”这栏,单击“Cycles & Roots”,就可以看到以图形方式显示出来的循环引用,这样我们就可以非常方便地找到循环引用的对象了。
4、进入自动引用计数(ARC)时代
ARC几乎把所有内存管理事宜都交由编译器来决定,开发者只需要专注于业务逻辑。
使用引用计数的方式进行内存管理,的确减轻了程序员很多的负担,但是依然要写很多重复的retain/release代码,那个时候的程序员需要小心的进行对象的retain和release,稍微不注意应用程序就崩溃了,那个时候常常在开发完成后,需要使用Instrument来检测泄漏,我们称那个时代为手动引用计数(MRC)时代。苹果在WWDC 2011年大会上,也就是在 OS X Lion 和 iOS 5 中引入内存管理的新技术,自动引用计数(ARC)。
顾名思义,自动引用计数(ARC,Automatic Reference Counting)是指内存管理中对引用采取自动计数的技术。以下摘自苹果的官方说明。
在Objectice-C中采用ARC机制,让编译器来进行内存管理。在新一代Apple LLVM编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。
也就是说,若满足以下条件,就无需手动输入retain和release代码了。
·使用Xcode 4.2 或者以上版本
·使用LLVM编译器 3.0 或者以上版本
·编译器选项中设置ARC为有效
在以上条件下编译源代码时,编译器将自动进行内存管理。
可以将自动引用计数技术看成是编译器LLVM的升级,将对象的引用计数(通过retain/release控制)直接交给了编译器完成,由编译器往源代码中自动添加retain/release代码。ARC技术已经比较成熟,从Mac OS X 10.8 开始,苹果正式废弃MacOS上的垃圾回收机制,以OC代码编写Mac OS X 程序时不应再使用它,采用ARC替代,而iOS则从未支持过垃圾回收机制。并且一个很重要的考核就是,使用ARC后,基本不会出现内存泄漏了。并且值得一提的是,虽然ARC是与iOS5一同推出,但是由于ARC的实现机制是在编译期完成,所以使用ARC之后应用仍然可以支持iOS4.3。稍微要注意的是,如果要在ARC开启的情况下支持iOS4.3,需要将weak关键字换成__unsafe_unretained。
但是,ARC也并不是万能的!有两个方面需要注意:
第一个方面,就是ARC状态下的源代码需要与非ARC管理的对象交互;直接处理非ARC的源代码。因此MRC依然是每个iOS程序员的必备技能。
ARC能够解决iOS开发中90%的内存管理问题,但是另外还有10%的内存管理,是需要开发者自己手动应用引用计数方案进行管理的,这主要就是与底层Core Foundation对象交互的那部分,底层的Core Foundation对象由于不在ARC的管理下,所以需要自己维护这些对象的引用计数。对于Core Foundation对象的引用计数的修改,要相应的使用CFRetain和CFRelease方法,比如:
// 创建一个CTFontRef对象
CFFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
// 引用计数加1
CFRetain(fontRef);
// 引用计数减1
CFRelease(fontRef);
对于CFRetain和CFRelease两种方法,可以直观地认为,它们跟OC对象的retain和release方法等价。
所以对于底层Core Foundation对象,我们只需要延续以前手工管理引用计数的办法即可。这方面还有一个问题需要解决,那就是在ARC管理下的文件中需要使用Core Foundation对象时,没必要将整个文件都关闭ARC状态,此时我们就需要将Core Foundation对象转换成一个OC对象,这个时候我们就需要告诉编译器,转换过程中的引用计数需要如何调整。这就引入了与bridge相关的关键字,以下是这些关键字的说明:
__bridge:只需要类型转换,不修改相关对象的引用计数,原来的Core Foundation对象在不用时,需要调用CFRelease方法。
__bridge_retained:类型转换后,将相关对象的引用计数加1,原来的Core Foundation对象在不用时,需要调用CFRelease方法。
__bridge_transfer:类型转换后,将该对象的引用计数交给ARC管理,Core Foundation对象在不用时,不再需要调用CFRelease方法。
我们根据具体的业务逻辑,合理使用上面的三种转换关键字,就可以解决Core Foundation对象与OC对象相对转换的问题了。
现在新建一个工程,默认的工程都是开启了自动引用计数ARC,为了兼容第三方非ARC开源库,需要修改工程设置,通常方式是指定源文件为MRC模式。如下图所示,在指定的源文件后加上-fno-objc-arc的编译参数,这个参数可以对该文件启用手工管理引用计数的模式。
也可以直接让整个工程都不使用ARC,方法就是:
第二个方面,虽然进入ARC时代后,程序员不再需要小心翼翼的写下大量成对的retain/release,而只需要按照ARC的规则,写下一些简单的代码(并不是说什么都不用写~)。但是在复杂的、容易出现循环引用的情况下,比如过度使用block的情况下,如果程序员不能娴熟的使用ARC编程技术,依然会掉入“循环引用、内存泄漏”这个坑中的,前面已经提到过,循环引用导致内存泄漏,是引用计数方案的一个瑕疵,不仅在MRC时代容易掉坑,在ARC中,技术不娴熟的程序员也还是会掉坑。
三、手动引用计数(MRC)
上面已经说明了,手动引用计数管理,无非就是手动的写下retain/release/autorelease代码,这一点都不假,但是并不容易,难的就是你得一丝不苟的“计算着”每个对象被引用的次数,难的就是对象的引用计数的递增和递减操作不是那么明显。
首先,关于引用计数的机制的理解,可以用“开关房间的灯”的例子来理解,这个也算是经典的例子,在《OC 高级编程》这本书中用的就是这款例子。例子内容不再赘述,但是“对象”和“对象的使用环境”这两个概念要弄清楚。在OC中,“对象”相当于办公室的照明设备。“对象的使用环境”相当于上班进入办公室的人,虽然这里的“环境”有时也指运行中的程序代码、变量、变量作用域、对象等,但在概念上就是使用对象的环境。
关于手动引用计数,我认为主要能把握以下4个点,充分理解和梳理清楚后就算是明白了。
0 从道理上来说,很简单,无非就是retain和release成对出现,谁创建谁释放的原则
1 隐形的递增递减对象的引用计数
2 autorelease的使用,与runloop的结合,自动清理自动释放池
3 MRC下的属性限定符起到的作用
先说第1点:
“从道理上来说,很简单,无非就是retain和release成对出现,谁创建谁释放的原则”。
一般概念就是:当创建对象时,初始的引用计数为1。为保证对象的存在,每当引用到该对象需要为该对象的引用数加1,可以给该对象发送retain消息:
[myObject retain];
当某个之前对该对象的引用不再需要的时候,通过给该对象发送release消息,为该对象的引用计数减1,语句如下:
[myObjec release];
当该对象的引用计数为0时,系统就知道这个对象不再需要使用了(依照理论,在应用中不再有引用到这个对象),所以可以释放它的内存。通过给对象发送dealloc消息发起这个过程。在多数情况下,会使用继承自NSObject的dealloc方法。然而,为了能够释放由对象创建或保持的实例变量或者其他对象,需要重载dealloc方法。例如,在你的类中,有一个实例变量为NSArray的对象,并且为它创建了一个数组,当对象销毁时,你需要负责释放数组,在dealloc方法中可以做这些事情。
手工管理引用计数策略成功与否取决于程序员,需要确保在程序运行时,引用计数能够合理递增和递减。系统能够帮助你做一些事情,但不是全部,其他的事情需要由你来做。
因此,一个对象从被创建到被销毁,对该对象做的递增递减操作肯定是成对存在的,这样才能让这个对象经过各种递增递减之后,最终“圆满”的引用计数为0,被回收内存。
关于第2点:
“隐形的递增递减对象的引用计数”。
什么意思呢,就是比如当你使用手动管理引用计数时,需要注意到Foundation框架中的一些方法可能会增加对象的引用计数,比如NSMutableArray的addObject:方法用于将一个对象添加到数组,或者UIView的addSubView:方法用于将一个视图作为子视图添加到另一个视图中使用。有一些方法会减少对象的引用计数,如removeObjectAtIndex:方法和removeFromSuperview:方法。
在《OC高级编程》这本书中说到“内存管理的思考方式”这块内容时,有四句话很经典:
·自己生成的对象,自己所持有
·非自己生成的对象,自己也能持有
·不再需要自己持有的对象时释放
·非自己持有的对象无法释放
这四句经典的话,前两句说的就涉及到“隐形的递增递减对象的引用计数”。
上面讲了retain和release的使用,那么autorelease在什么场景下使用呢?可能会编写这样一个方法,先创建一个对象(使用alloc),然后将它作为方法调用的结果返回。这样就进入了一个困境:尽管方法不再使用这个对象,但是并不能释放它,因为需要将这个对象作为方法的返回值。NSAutoreleasePool类创建的目的就是希望能够解决这个问题,自动释放池可以帮助追踪需要延迟一些时间释放的对象。然后时机到了之后,通过给自动释放池发送drain消息,自动释放池会被清理,对象会被释放。
给对象发送autorelease消息,即是将该对象添加到由自动释放池维护的对象列表中,语句如下:
[myObjec autorelease];
四、自动引用计数(ARC)
-----未完待续,2021年6月12日
-----未完待续,2021年6月13日