【iOS】内存管理

文章目录

  • 前言
  • 理解引用计数
    • 引用计数原理
    • 属性存取方法中的内存管理
    • 自动释放池
    • 保留环
  • 以ARC简化引用计数
    • 使用ARC时必须遵守的命名规则
    • 变量的内存管理语义
    • ARC如何清理实例变量
    • 覆写内存管理的方法
  • 在dealloc方法中只释放应用并解除监听
  • 编写“异常安全代码”时留意内存管理问题
  • 以弱引用避免保留环
  • 以“自动释放池块”降低内存峰值
  • 用“僵尸对象”调试内存管理问题
  • 不要使用retainCount


前言

内存管理:
在Objective-C这样的面向对象的语言里,内存管理是很重要的概念。理解了内存管理模型的种种细节之后,Objective-C的内存管理就没有那么复杂了。尤其是有了“自动引用计数(ARC)”之后,更为简单。ARC几乎把所有的内存管理事宜都交给了编译器处理,开发者只需注重业务逻辑。


提示:以下是本篇文章正文内容,下面案例可供参考

理解引用计数

Objective-C使用引用计数来管理内存。每个对象都有可以递增或递减的计数器。
如果要使某个对象存活,递增其引用计数。用完之后,递减其计数,计数变为0的时候,就没人关注此对象了,销毁它。

引用计数原理

在引用计数架构下,对象有一个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在Objectiv-C中叫做“保留计数”,也可以叫“引用计数”。

Retain 递增保留计数
release 递减保留计数
autorelease 待稍后清理“自动释放池”时,再递减保留计数。
  • 查看保留计数的方法叫做retainCount,此方法不太有用。
  • 对象创造出来时,引用计数至少为1.若想令其继续存活,则调用retain方法。要是某部分代码不在使用此对象,不想令其继续存活,就调用release或者autorelease方法。当保留计数归零时,对象就回收(deallocated)了,系统会将占用的内存标记为“可重用”。此时,所有指向该对象的引用也都变得无效了。
    【iOS】内存管理_第1张图片
  • 应用程序在其生命周期中会创建很多对象,这些对象都相互联系着。相互关联的对象就构成了一张“对象图”。对象如果持有指向其他对象的强引用,那么前者就“拥有”后者。也就是说,对象向令其所引用的那些对象继续存活,就可以将其“保留”。等用完之后,在做释放。【iOS】内存管理_第2张图片
  • 在图示中,ObjectB和ObjectC都引用了ObjectA。若ObjectB和ObjectC都不在使用ObjectA,则其保留计数降为0,此时可以摧毁ObjectA。如果还有其他对戏那个想令ObjectB和ObjectC继续存活,而应用程序里又有另外一些对象想令那些对象继续存活。如果按“引用计数”回溯,那么最终会发现一个“根对象”。在Mac OS X应用程序中,此对象就是NSAppliction对象。而在iOS应用程序中,是UIApplication对象。两者都是应用程序启动时创建的单例。
  • 如果代码中直接调用release方法,ARC下就无法编译。在Objective-C中,调用alloc方法所返回的对象由调用者所拥有。也就是说,调用者已经通过alloc方法表达了想令该对象继续存活下去的意愿。此时保留计数至少为1。保留计数绝不一定是某个值,只能说执行的操作是递增了还是递减了该计数
  • 如果调用release方法之后,基于某些原因,其保留计数降为0,此时对象所占内存可能会回收。这样的话,在调用NSLog kennel就会使程序崩溃。对象所占的内存在“解除分配”后,只是放回“可用内存池”,如果执行NSLog时还未覆写对象内存,那么该对象仍然有效,此时程序不会崩溃。因过早释放对象所导致的bug很难查找
  • 为避免使用无效对象,一般调用完release之后都会清空指针。就可以保证不会出现指向无效对象的指针,这种指针通常称为“悬挂指针”。

属性存取方法中的内存管理

  • 对象图由互相关联的对象构成,树枝通过在其元素上调用retain方法来保留那些对象。其他对戏那个也可以保留别的对象,一般通过属性来实现。访问属性时,会调用相关实例变量的获取方法及设置方法。若属性为“strong”关系,则设置的属性值会保留。
 - (void)setFoo:(id)foo{
    [foo retain];
    [_foo release];
    _foo = foo;
}
  • 此方法会保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要,假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么先执行的release操作就可能导致系统将此对象永久回收。而后的retain操作无法令已经彻底回收的对象复生,于是实例变量就变成了悬挂指针。

自动释放池

调用release方法会立即递减保留计数(还有可能让系统回收此对象)。如果改用autorelease,此方法会稍后递减计数,通常是在下一次“事件循环”时递减,也可能更早一些。

 - (NSString*)stringValue {
    NSString* str = [[NSString alloc] initWithFormat:@"I am this :%@", self];
    return str;
}
  • 这个方法返回的str对象的保留计数比预期值多1。因为调用alloc方法会令保留计数加1,没有与之对应的释放操作。需要想办法如何将多出来的这个一次保留抵消掉。
  • 不能在方法内释放str,否则还没等方法返回,系统就把对象回收了。这里应该使用autorelease,他会在稍后释放对象。从而给调用者留下足够长的时间,使其可以在需要时先保留返回值。此方法可以保证对象在跨越“方法调用边界”后一定存活。释放操作会在清空最外层的自动释放池时执行,除非有自己的释放池,否则这个时机就是当前线程的下一次事件循环。
 - (NSString*)stringValue {
    NSString* str = [[NSString alloc] initWithFormat:@"I am this :%@", self];
    return [str autorelease];
}
  • 此时,此对象必然存活。由于返回的str对象将与稍后自动释放,无需再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行。NSLog中使用str对象前就不需要手工执行保留操作。如果要持有此对象,那就需要保留,并于稍后释放。

保留环

保留环是值呈现环状的相互引用的多个对象。他容易导致内存泄漏。因为循环中的对象的保留计数不会降为0.对于每个循环中的每个对象来说,至少还有另一个对象引用着它。

【iOS】内存管理_第3张图片

  • 图示即为保留环,在垃圾收集系统下,所有的对象的引用计数至少为1。在垃圾收集系统中,通常将这种情况认定为“孤岛”。此时,垃圾收集器会把三个对象全部回收走。在Objective-C的引用计数中,通常采用“弱引用”来解决问题,或是从外界命令循环中的某个对象不在保留另一个对象。这两种方法都能打破保留环,从而避免内存泄漏。

以ARC简化引用计数

需要执行保存和释放操作的地方很容易就能看出来。Clang编译器自带一个“静态分析器”,用于指明程序里引用计数出问题的地方。

if ([self shouleLogMessage]) {
    NSString* message = [[NSString alloc] initWithFormat: @"I am object, %p", self ];
    NSLog (@"message = %@". message);
}
  • 此代码存在内存泄漏问题。因为if语句末尾并未释放message对象。而在if语句之外又无法调用message对象,此时message对象所占的内存就泄漏了。如果调用NSString的alloc方法所返回的message对象的保留计数比期望值多1,内存泄漏。
  • “静态分析器”要做的事就是套用判断内存是否泄漏的规则,分析出内存泄漏问题的对象。
  • 自动引用计数这一思路是“静态分析器根据需要,预先加入适当的保留或者释放操作以避免一些问题。”自动引用计数所做的事情与其名称相符,就是自动管理引用计数。
  • 使用ARC时一定要记住,引用计数实际上还是执行的,只不过保留和释放操作现在是由ARC自动添加。除了为方法所返回的对象正确运用内存管理语义之外,ARC还有更多的功能。
  • ==ARC会自动执行retain,release。autorelease,delloc等操作。在ARC下调用这些内存管理方法都是非法的。==直接调用这些方法都会产生编译错误,ARC要分析何处应该自动调用内存管理方法,所以如果手动调用,会干扰其工作。
  • 在ARC调用这些方法时,不通过OC的消息派发机制,而是直接调用底层C语言版本。这样做性能更好。

使用ARC时必须遵守的命名规则

  • 将内存管理语义在方法名中表示出来早已成为OC的管理,ARC将它确立为硬性规定。这些规则简单的体现在方法名上,如果方法名以下列词语开头,则其返回的对象归调用者所有。如:alloc,new,copy,mutableCopy。
  • 归调用者所有的意思是:调用上述四种方法的代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留的操作抵消掉。如果还有其他对象保留此对象,并对其调用了autorelease,那么其保留计数的值可能比1大。
  • 若方法名不以上述四个词语开头,则其返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,其值在跨越方法时调用边界后依然有效。如果想让对象多存活一段时间,必须令调用者保留它才行。
  • ARC通过命名约定内存管理规则标准化。除了会自动调用“保留”与“释放”之外,使用ARC可以执行一些手工操作很难甚至无法完成的优化。ARC也包含运行期组件,此时执行的优化很有意义。在ARC环境下编译代码时,必须考虑“向后兼容性”,以兼容那些不适应ARC的代码。
  • ARC可以在运行期检测到这一对多余的操作,也就是autorelease及紧跟其后的retain。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的autorelease方法,而是改用objc_autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码。如果发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构中的一个标志位,而不执行autorelease操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行retain,而改为执行objc_retainAutoreleaseedReturnValue函数。此函数要检测刚才提到的标志位,如果已经置位,则不执行retain操作。并设置检测标志位,要比autorelease和retain快
  • 将内存管理交由编译器和运行期组件来做,,可以使代码得到多种优化。

变量的内存管理语义

  • ARC也会处理局部变量和实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。
  • ARC会用一种安全的方法来设置:先保留新值,再释放旧值,最后设置实例变量。在应用程序中,可以用下列修饰符来改变局部变量和实例变量的语义。
_ _strong: 默认语义,保留其值
_ _unsafe_unretained: 不保留此值,这么做不太安全,因为等到再次使用变量时,变量有可能已经被回收了
__weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空
__autoreleasing: 把对象“按引用传递”给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放。

【iOS】内存管理_第4张图片

  • 不论采用哪种写法,在设置实例变量时都不会保留其值。只有在使用新版运行期程序库时,加了_weak修饰的weak引用才会自动清空。
  • 我们经常给局部变量加上修饰符,用以打破由“块”所引入的“保留环”。块会自动保留其所捕获的对象,而如果其中有某个对象又保留了块本身,就可能导致“保留环”。可以使用_weak局部变量来打破这种“保留环”

ARC如何清理实例变量

要管理内存,ARC就必须在“回收分配给对象的内存”时生成必要的清理代码。凡是具备强引用的变量,都必须释放,ARC会在dealloc方法中插入这些代码。

  • 用了ARC后,就不要编写[obj dealloc]这样的方法了。因为ARC会借用Objective-C++的一项特性来生成清理例程。回收Objective-C++对象时,待回收的对象就会调用所有C++对象的析构函数。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。
  • 如果有非Objective-C的对象,仍需要清理内存。

覆写内存管理的方法

不使用ARC时,可以覆写内存管理方法。在实现单里类的时候,因为单例不可释放,所以我们经常覆写release方法。将其替换为“空操作”。但在ARC环境下不能这样做,因为会干扰到ARC分析对象生命周期的工作。

在dealloc方法中只释放应用并解除监听

对象在经历其生命周期后,最终会被系统回收。此时要执行dealloc方法。在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为0时。

  • 我们应该在dealloc方法中释放对象所拥有的引用,也就是把所有的Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法,在dealloc中为你自动添加这些释放代码。对象所拥有的其他非Objective-C的对象也要释放。
  • 在dealloc方法中,我们通常还要把原来配置过的观测行为都清理掉。如果给对象发送某种通知,一般都应该在此处注销通知。
  • 如果手动管理引用计数不使用ARC,那么最后还要调用[super dealloc]。ARC会自动执行此操作,这再次表明其比手动管理更简单,更安全。若选择手动管理,还要将当前对象所拥有的全部Objective-C对象逐个释放。
  • 开销较大或系统内存稀缺的资源不在dealloc中释放引用。比如文件描述符,套接字,大块内存等,都属于这种资源。不能依赖dealloc方法必定会在某个特定的时机调用,因为有一些无法预料的对象可能也持有此对象。在这种情况下,如果我们一定要等到系统调用dealloc方法的时候才释放,那么保留这些稀缺资源的时间就过长了,这么做不合适。通常的做法是:实现另外一个方法,当应用程序用完资源对象后,就调用此方法。
  • 在清理方法而非dealloc方法中清理资源是因为系统不能保证每个创建出来的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到dealloc消息。当应用程序终止后,其占用的资源也会返回给操作系统,所以实际上这些对象也就等于是消亡了。不调用dealloc方法是为了优化程序效率。这也说明了系统未必会在每个对象上调用dealloc方法。
  • 如果对象管理着某些资源,那么在dealloc中也要调用“清理方法”,以防止开发者忘了清理这些资源。有时可能不想只输出一条错误消息,而是要抛出异常来表明不调用某个方法是严重的错误
  • 编写dealloc方法时还应该注意,不要在其中随便调用其他方法。如果调用了其他方法,那么等到那些人物执行完毕的时候,系统研究把当前这个待回收的对象彻底摧毁了。经常导致应用程序崩溃,因为那些任务执行完毕后,要回调此对象,告诉该对象任务已经完成,而此时如果对象已经摧毁,回调操作就会出错。
  • 调用dealloc方法里的那个线程会执行“最终的释放操作”,令对象的保留计数为0。但是某些方法必须在特定的线程里调用才行。若在dealloc里调用了那些方法,无法保证当前这个线程就是那些方法所需要的线程。
  • 在dealloc方法里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。属性可能正处于“键值观测”机制的监控之下,该属性的观察者可能会在属性值改变时“保留”或使用这个歌即将回收的对象,这种做法会令运行期系统状态完全失调,从而导致一些莫名其妙的错误。

编写“异常安全代码”时留意内存管理问题

Objective-C和C++支持“异常”这一特性。在当前的运行期系统里,C++和Objective-C的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的“异常处理程序”来捕获。

  • Objective-C的错误模型表明,异常应该在发生严重错误后抛出。但是有时仍然需要编写代码捕获并处理异常。当使用Objective-C++编码时,或者编码中使用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。有些系统库也会用到异常。
  • ARC能生成安全处理异常的附加代码。-fobjc-are-exceptions这个编译标志用来开启此功能。但是默认情况下不开启这个功能。因为在OC中,只用当应用程序必须因异常状况而终止时才应该抛出异常。因此,如果应用程序即将终止,那么是否还会发生内存泄漏就已经无关紧要里。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码时没有意义的。
  • 当处于Objective-C++模式时,编译器会自动把-fobjc-arc-exceptions标志打开。
  • 如果手动管理引用计数,而且必须捕获异常,那么要设法保证所编代码能把对象正确清理干净。若使用ARC切必须捕获异常,打开编译器的-fobjc-arc-exceptions标志

以弱引用避免保留环

几个对象都以某种方式相互引用,形成“环”。这种情况会泄漏内存,因为最后没有别的东西引用环中的对象。环里的东西无法为外界所访问,但是对象之间尚有引用,这些引用使他们继续存活下去,不被系统回收。

  • 最简单的保留环是两个对象互相引用。【iOS】内存管理_第5张图片
  • 把classA的一个属性设置成ClassB的类的实例,把ClassB的一个属性设置成ClassA的一个实例,就会出现保留环。【iOS】内存管理_第6张图片
  • 保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环都泄漏了。也就是说,没办法在继续访问其中对象了。
  • 避免保留环的方式就是弱引用。弱引用经常表示“非拥有关系”。将属性声明称为unsafe_unretained。这个词表示,属性可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于此对象不保留属性对象,因此极有可能为系统所回收。
  • unsafe_unretained修饰的属性特性,语义和assign特质等价。assign用于“整型类型”(int,float,结构体等),unsafe_unretained多用于对象类型。这个词本身表明其修饰的属性可能无法安全使用。
  • OC中还有一项和ARC相伴的运行期特性,可以令开发者安全使用弱引用。weak属性特质。它与unsafe_unretained的作用完全相同。然而只要系统把属性回收,属性值就会自动设为nil。【iOS】内存管理_第7张图片
  • 当指向ClassA的实例引用移除后,unsafe_unretained属性仍然指向那个已经回收的实例,weak指向nil。
  • 使用weak比unsafe_unretained更安全。应用程序可能现实错误的数据,不会直接崩溃。==只要在所指对象已经彻底崩溃后还继续使用弱引用,那它就依然是一个bug。
  • 一般来说,如果不再拥有某对象,那就不要保留它。collection除外,collection虽然不直接拥有其内容,但是他要代表自己所属的那个对象来保留这些元素。
  • weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,有运行期系统来实现。在具备自动清空的弱引用上,可以随意读取数据,这种引用不会指向已经回收过的对象。

以“自动释放池块”降低内存峰值

Objective-C对象的生命周期取决于引用计数。释放对象有两种方式:一种是调用realse方法,使其保留计数立即递减。另一种是调用autorelease方法,将其加入“自动释放池”。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会自动向其中的对象发送realse消息。

  • 如果没有创建自动释放池,向对象发送autorelease消息,控制台会输出一条信息。一般情况下不用担心自动释放池的创建问题。系统会自动创建一些线程,这些线程默认有自动释放池。
  • 每次执行“事件循环”时,就会将其清空。不需要自己创建“自动释放池”。通常只有在main函数里需要创建自动释放池。
  • main函数的末尾恰好就是应用程序的终止处,此时操作系统会把程序所占的全部内存都释放掉。自动释放池于左花括号处创建,右花括号处自动清空。位于自动释放池内的对象,在此范围末尾处收到realse消息。
  • 内存峰值是指应用程序在某个特定的时间段内的最大内存用量。
  • 自动释放池就像“栈”用于,系统创建好自动释放池后,就将其推入栈中,而清空自动释放池,相当于将其从栈中弹出。在对象上执行自动释放操作,就相当于将其放入栈顶的那个池里。
  • 是否应该使用池来优化效率,取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题。进了不要额外的建立自动释放池。
  • 还有一种写法,使用NSAutoreleasePool对象。它专门用来表示自动释放池,就像新语法中的自动释放池块一样。这种写法不会在每次for循环时都清空池,此对象更为“重量级”,通常用来创建那种偶尔需要清空的池。
  • @autoreleasepool还有一个好处,每个自动释放池均有其范围,可以避免无意间误用了那些清空池后已经 被系统回收的对象。

用“僵尸对象”调试内存管理问题

  • 业内向以回收的对象发送消息是不安全的,这么做有时可以,有时不行。能否这样做取决于对象所占内存也没有为其他内容所覆写。这块内存也没有另作它用又无法确定,因此,程序只是偶尔崩溃。在没有崩溃时,那块内存可能只是复用了其中一部分,所以对象中的某些二进制数据依然有效。还可能是那块内存恰好为另外一个有效且存活的对象占据。这个时候,运行期系统会把消息发到新对象那里,而此对象也许能回答,也许不能。如果能,程序就不会崩溃。
  • 启用调试功能后,运行期系统会把所有的已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收他们。这种对象的核心内存无法复用,所以不可能覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。将NSZombieEnabled环境变量设置为YES,就可以开启这个功能
  • 僵尸对象的实现代码深植于Objective-C的运行期程序库,Foundation框架及CoreFoundation框架中。系统在相爱即将回收对象时,如果发现通过环江变量启用了僵尸对象功能,那么还将指向一个附加步骤。这一步就是把对象转化为僵尸对象,不在彻底回收。
  • 僵尸类是从名为_NSZombie的模板类里复制出来的。这些僵尸类没有多少事情可以做,只是充当一个标记。
  • 给僵尸对象发送消息后,系统可以由此知道该类对象所属的原来的类。加入吧所以僵尸对象都归到_NSZombie类里,那原来的类名就丢失了。僵尸类没有超类,是一个“根类”,只有一个实例变量,叫做isa,所有OC得根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发送给他的全部信息都要经过“完整的消息转发机制”。

不要使用retainCount

  • 方法:“-(NSUInteger)retainCount”不能在ARC种调用。
  • 保留计数的绝对值数值一般都于开发者所应留意的事情完全无关。即便只在调试时才调用此方法,通常也没有什么用。
  • 这个方法返回的保留计数只是某个给定时间的上的值。该方法并未开绿道系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去。这样的话,就未必能真实反映实际的保留计数了。
  • 对象的保留计数看似有用,实则不然,因为任何给定时间的上的“绝对保留计数”(abusolute retain count)都无法反应对象生命周期的全貌。
  • 在引入ARC之后,苹果公司已经废弃了retainCount,所以绝对不要使用这个方法。

你可能感兴趣的:(iOS,ios)