文章目录
- 二十九、理解引用计数
- 三十、以ARC简化引用计数
- 三十一、在dealloc方法中只释放引用并解除监听
- 三十二、编写异常安全代码时留意内存管理问题
- 三十三、以弱引用避免保留环
- 三十四、以”自动释放池块“降低内存峰值
- 三十五、用"僵尸对象"调试内存管理问题
- 三十六、不要使用retainCount
二十九、理解引用计数
Objective-C 语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器,如果某个对象引用他时就会给其引用计数加1,用完了之后,就递减其计数,直至为0,销毁这个对象。ARC实际上也是一种引用计数机制。
引用计数工作原理
在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。NSObject协议声明了下面三个方法用于操作计数器:
Retain
递增保留计数
release
递减保留计数
autorelease
待稍后清理“自动释放池”时,再递减保留计数。
对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain
方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用release
或autorelease
方法。最终当保留计数归零时,对象就回收了(deallocated),也就是说,系统会将其占用的内存标记为“可重用”。此时,所有指向该对象的引用也都变得无效了。
在图5-2所示的对象图中,ObjectB与ObjectC都引用了ObjectA。若ObjectB 与ObjectC 都 不 再 使 用 O b j e c t A , 则 其 保 留 计 数 降 为 0 , 于 是 便 可 摧 毁 了。
为了避免在不经意间使用了无效对象,一般relase之后都会清空指针,这样能保证不出现悬挂指针
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2023];
[array addObject:number];
[number release];
number = nil;//避免悬垂指针
属性存取方法中的内存管理
属性存取方法也存在内存管理模式,对象也可以保留别的对象,这一般通过访问属性来实现,而对于实例变量中的属性为strong关系的时候设置的值会保留
@property (nonatomic, strong)NSString *foo;
- (void)setFoo:(id)foo {
[foo retain];//保留新值
[_foo release];//释放旧值
_foo = foo;//更新实例变量
}
此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要,我们不能先释放旧的值,否则对象那么有可能在release之后就被销毁了,可能存在悬空指针的现象
自动释放池
在Objective-C 的引用计数架构中,自动释放池是一项重要特性。调用release 会立刻递 减对象的保留计数 ( 而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用 autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环” (event1oop)时递减, 不 过 也 可 能 执 行 得 更 早 些 (參 见 第 3 4 条 )。
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this:%@", self];
return str;
}
调用此方法时会让引用计数多1,我们需要设法抵消这多出来的1,这就需要用到autorelease
此时返回的str对象其保留值比期望多1 因为alloc会➕1,然后并没有进行释放,意味着调用者可能背负的操作更多,但又不能在返回前就release str,所以代码可以这样写
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this:%@", self];
return [str autorelease];
}
实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。
通过上述可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。
保留环
保留环其实就是因为对象之间的互相引用出现的问题,这会导致内存泄漏,因为循环中的对象其保留计数不会降为0。
我们要解决保留环,通常采用“弱引用”来解决此问题,或者从外界命令循环中的某个对象不在保留另一个对象。
要点
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
三十、以ARC简化引用计数
使用ARC时一定要记住,引用计数实际上 还是要执行的,只不过 保 留 与 释 放 操 作 现 在 是由ARC 自动为你添加。稍后将会看到,除了为方法所返回的对象正确运用内存管理语义 之外,ARC 还有更多的功能。 不过,ARC 的那些功能都是基 于核心的内存管理语义而构建的, 这套标准语义贯穿于整 个Objective-C语言。
由于ARC
会自动执行retain 、release 、autorelease
等操作,所以直接在ARC
下调用这些 内存管理方法是非法的。具体来说,不能调用下列方法:
- retain
- release
- autorelease
- dealloc
实际上,ARC 在调用这些方法时,并不通过普通的Objective-C 消息派发机制,而是直 接调用其底层C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直 接 调 用 底 层 丽 数 能 节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数 objc_retain。这也是不能覆写retain、release 或autorelease 的缘由,因为这些方法从来不会被直接 调用。笔者在本节后面的文字中将用等价的Objective-C 方法来指代与之相关的底层C语言 版本,这对 于那些 手动管理过引用计数的开发者来说更易理解。
使用ARC时必须遵守的方法命名规则
若方法名以下列词语开头,则其返回的对象归调用者所有:
- alloc
- new
- copy
- mutableCopy
意思就是调用这四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消。若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。这种情况下,返回的对象会自动释放,也就是使用了autorelease。
ARC除了自动调用“保留”和“释放”方法外,其也可以优化操作,比如两个在一起的保留和释放,它就会将这一对直接移除,不执行这两个代码。
在ARC环境下编译代码时,必须考虑“向后兼容性”,以兼容那些不使用ARC的代码,其实ARC的简化操作是因为其调用的特殊函数,它会把autorelease方法改为调用objc_autoreleaseReturnValue函数,把retain方法改为objc_retainAutoreleaseReturnValue函数。
变量的内存管理语义
- ARC也会处理局部变量和实例变量的内存管理,默认情况喜爱每个变量都是指代对象的强引用,一定要理解的事实例变量的真正意思是什么。对于某些代码来说,语义和手动内存管理存在一定差异。例如在编写setter方法的时候,不用ARC模式是这样写
- (void)setObject:(id)object {
[_object release];
_object = [object retain];
}
这样写的问题是新值和实例变量的值是一样的,只有当前对象还在用引用这个值的时候,那么设置方法中的释放操作会令值保留计数变为0,那么就会被系统回收从而产生crash,接下来retain操作则是错误的,使用ARC的时候不会存在这种流失的现象
- (void)setObject:(id)object {
_object = object;
}
ARC会用一种安全的方式来设置,先保留新值,再释放旧值,最后设置实例变量
ARC如何清理实例变量
要管理其内存,ARC就必须在“回收分配给对象的内存”是生产必要的清理代码。ARC环境下,dealloc方法可以这样来写:
- (void)dealloc {
CFRelease ( _coreFoundationObject);
free ( _heapAllocatedMemoryBlob);
}
因为ARC会自动生成回收对象时所执行的代码,所以通常无需再编写dealloc方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码。
要点:
- 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。
- ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。
- ARC只负责管理OC对象的内存。尤其要注意CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
三十一、在dealloc方法中只释放引用并解除监听
对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc
方法了。在每个 对象的生命期内,此方法仅执行一次,也就是当保留计数降为0的时候。然而具体何时执 行,则无法保证。也可以理解成:我们能够通过人工观察保留操作与释放操作的位置,来预 估此方法何时即将执行。但实际 上,程序库会以开发者察觉不到的方式操作对象,从而使回 收对象的真正时机和预期的不同。你决不应该自己调用dealloc 方法。运行期系统会在适当 的时候调用它。而且, 一旦调用过dealloc之后,对象就不再有效了,后续方法调用均是无效的。
那么,应该在dealloc 方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把 所 有 Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法 (参见第30条), 在dealloc 中为你自动添加这些释放代码。对象所拥有的其他非Objective-C 对象也要释放。 比如CoreFoundation 对象就必领手工释放,因为它们是由纯C的API 所生成的。
在dealloc 方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior )都清理掉。如果用NSNotificationCenter 给此对象订阅(register)过某种通知,那么 一般应该在这里注销(unregister ),这样的话,通知系统就不再把通知发给回收后的对象了, 若是还向其发送通知,则必然会令应用程序崩溃。
dealloc 方法可以这样来写:
- (void)dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
要点:
-
在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”或“NSNotificationCenter”等通知,不要做其他事情。
-
如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
-
执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。
关键是释放引用并且解除监听
三十二、编写异常安全代码时留意内存管理问题
要点
- 捕获异常时,一定要注意将 try 块内所创立的对象清理干净。
- 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
三十三、以弱引用避免保留环
- 保留环这个词出现了很多次了,究极原因是对象之间的相互引用造成的
- 最简单的保留环可以由两个对象构成,它们互相引用对方
- 对于如下的代码可以看出引用环的存在
保留环导致内存泄漏
保留环会导致内存泄漏,如果只剩一个引用还指向保留环的实例,而现在这个又把引用一处,那么整个环就会泄漏,也就是说无法继续访问其中的任何对象了
学会避免保留环的出现
- 避免保留环的最佳方式就是弱引用。这种引用通常用来表示“非拥有关系“,将属性声明为unsafe_unretained即可,当然注意unsafe_unretained修饰的属性同assgin特质等价
#import <Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end
@interface EOCClassB: NSObject
@property(nonatomic,unsafe unretained) EOCClassA *other;
@end
- OC还有一项与ARC相伴的运行期的特性,叫弱引用weak,与unsafe_unretained作用完全相同,在现在的ARC下基本取代了unsafe_unretained关键字。对于weak只要系统回收了属性,属性值自然置nil。
weak 和unsafe_unretained区别
要点
- 将某些引用设为weak,可避免出现“保留环”。
- weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
三十四、以”自动释放池块“降低内存峰值
Objective-C对象的生命期取决于其引用计数(参见第29条)。在 Objective-C的引用计数架构中,有一项特性叫做"自动释放池"(autorelease pool)。释放对象有两种方式;一种是调用release 方法,使其保留计数立即递减;另一种是调用 autorelease 方法,将其加入"自动释放池"中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送 release 消息。
创建自动释放池所用语法如下∶
@autoreleasepool {
// ...
}
然而,一般情况下无须担心自动释放池的创建问题。Mac OS X与 iOS 应用程序分别运行于Cocoa 及 Cocoa Touch 环境中。系统会自动创建一些线程,比如说主线程或是"大中枢派发"(Grand Central Dispatch,GCD)③机制中的线程,这些线程默认都有自动释放池,每次执行"事件循环"(event loop)时,就会将其清空。因此,不需要自己来创建"自动释放池块"。通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们用自动释放池来包裹应用程序的主入口点((main application entry point)。比方说,iOS程序的 main 函数经常这样写∶
int main(int argc,char *argv[]) {
@autoreleasepool {
return UIApplicationMain (argc, argV, ni1, @"EOCAppDelegate");
}
}
比方说,要从数据库中读出许多对象。代码可能会这么写∶
NSArray*databaseRecords =/* ...*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
EOCPerson *person = [ [EOCPerson alloc] initWithRecord: record];
[people addObject:person];
}
这就意味着在执行for 循环时。会持续有新对象创建出来。并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来,在执行 for 循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
EOCPerson的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在"自动释放池块"中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如∶
NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords)[
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量(highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。
自动释放池机制就像"栈"(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
要点
- 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
三十五、用"僵尸对象"调试内存管理问题
调试内存管理问题很令人头疼。大家都知道,向业已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里。而此对象也许能应答。也许不能。如果能。那程序就不崩溃,可你会觉得奇怪∶为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。
所幸 Cocoa提供了"僵尸对象"(Zombie Object)这个非常方便的功能。启用这项调试功能之后,**运行期系统会把所有已经回收的实例转化成特殊的"僵尸对象"、而不会真正回收它们。**这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
僵尸对象的工作原理:
系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。
僵尸类是怎么产生的:
他其实是在运行期生成的,当首次碰到一个类的对象要变成僵尸对象时,它就会创建这么一个类。僵尸类是从名为_NSZombie_的模版里复制出来的,这些僵尸类没有多少事情可做,只是一个标记,又因为它将原类的方法都拷贝了,所以它会响应原类的方法,不过它会报错提醒程序员。
要点:
- 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled 可开启此功能。
- 系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为∶打印一条包含消息内容及其接收者的消息,然后终止应用程序。
三十六、不要使用retainCount
在ARC模式下该方法已经被弃用了,首要原因还是它返回的保留计数只是某个对象在某个具体时间的引用计数,完全没有特殊的参考意义,现在不仅存在自动释放池,还有ARC模式的自动管理引用计数,那么retainCOunt完全不能反映真实的引用计数。
要点
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌。
- 引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。