文章目录
- 前言
- 理解引用计数
- 引用计数原理
- 属性存取方法中的内存管理
- 自动释放池
- 保留环
- 以ARC简化引用计数
- 使用ARC时必须遵守的命名规则
- 变量的内存管理语义
- ARC如何清理实例变量
- 覆写内存管理的方法
- 在dealloc方法中只释放应用并解除监听
前言
内存管理:
在Objective-C这样的面向对象的语言里,内存管理是很重要的概念。理解了内存管理模型的种种细节之后,Objective-C的内存管理就没有那么复杂了。尤其是有了“自动引用计数(ARC)”之后,更为简单。ARC几乎把所有的内存管理事宜都交给了编译器处理,开发者只需注重业务逻辑。
提示:以下是本篇文章正文内容,下面案例可供参考
理解引用计数
Objective-C使用引用计数来管理内存。每个对象都有可以递增或递减的计数器。
如果要使某个对象存活,递增其引用计数。用完之后,递减其计数,计数变为0的时候,就没人关注此对象了,销毁它。
引用计数原理
在引用计数架构下,对象有一个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在Objectiv-C中叫做“保留计数”,也可以叫“引用计数”。
Retain | 递增保留计数 |
---|---|
release | 递减保留计数 |
autorelease | 待稍后清理“自动释放池”时,再递减保留计数。 |
- 查看保留计数的方法叫做retainCount,此方法不太有用。
- 对象创造出来时,引用计数至少为1.若想令其继续存活,则调用retain方法。要是某部分代码不在使用此对象,不想令其继续存活,就调用release或者autorelease方法。当保留计数归零时,对象就回收(deallocated)了,系统会将占用的内存标记为“可重用”。此时,所有指向该对象的引用也都变得无效了。
- 应用程序在其生命周期中会创建很多对象,这些对象都相互联系着。相互关联的对象就构成了一张“对象图”。对象如果持有指向其他对象的强引用,那么前者就“拥有”后者。也就是说,对象向令其所引用的那些对象继续存活,就可以将其“保留”。等用完之后,在做释放。
- 在图示中,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.对于每个循环中的每个对象来说,至少还有另一个对象引用着它。
- 图示即为保留环,在垃圾收集系统下,所有的对象的引用计数至少为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: | 把对象“按引用传递”给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放。 |
- 不论采用哪种写法,在设置实例变量时都不会保留其值。只有在使用新版运行期程序库时,加了_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里调用了那些方法,无法保证当前这个线程就是那些方法所需要的线程。