iOS内存管理理论知识过一遍

Posted 码出境界

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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、不要向已经释放的对象发送消息

因为该对象的内存已经被回收了,而我们向一个已经被回收的对象发送消息,得到的结果是不确定的,如果该对象所占的内存被复用了,那么就有可能造成程序异常崩溃。

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时代后,程序员不再需要小心翼翼的写下大量成对的retain/release,而只需要按照ARC的规则,写下一些简单的代码(并不是说什么都不用写~)。但是在复杂的、容易出现循环引用的情况下,比如过度使用block的情况下,如果程序员不能娴熟的使用ARC编程技术,依然会掉入“循环引用、内存泄漏”这个坑中的,前面已经提到过,循环引用导致内存泄漏,是引用计数方案的一个瑕疵,不仅在MRC时代容易掉坑,在ARC中,技术不娴熟的程序员也还是会掉坑。

 

 

 

三、手动引用计数(MRC)

 

 四、自动引用计数(ARC)

 

 

-----未完待续,2021年6月12日

 

以上是关于iOS内存管理理论知识过一遍的主要内容,如果未能解决你的问题,请参考以下文章

iOS中OC对象模型的理论知识过一遍

oracle基础知识过一遍(原创)

十分钟过一遍Kotlin知识点

CSS378- [译]44个 CSS 精选知识点

简单地过一遍C语言基础部分所有知识点,点到为止!(仅一万多字)

ios开发之OC基础-类和对象