Objective-C 内存管理
写在前面
本文是阅读 Advanced Memory Management Programming Guide 的笔记。
主要内容是关于手动管理内存的规则。
众所周知,Objective-C 它提供了2种内存管理方式:
- Manual Retain-release MRR
- Automatic Reference Counting ARC
目前 Xcode 默认使用 ARC ,而在 ARC 环境下,很多工作,编译器已经帮忙完成了。
而要真正了解内存管理规则,还得追根溯源,从 MRR 出发。
简介
内存管理可能出现的问题
- 释放或重写正在使用的内存数据,一般会造成应用闪退,更严重地,弄脏用户数据。
- 没有释放已经不再使用的内存,即造成 memory leaks。
内存问题检测工具
Xcode 附带的静态分析工具,可以分析出可能有问题的地方。
如果解决了静态分析工具找到的问题后,仍然有内存管理问题,可考虑使用下述工具或技术来定位问题:
- 官方调试技巧,尤其是其中的 NSZombie,可以找回已经释放了的对象。
- 使用 Instruments 去追踪引用计数情况,以及定位内存泄漏。
内存管理规则
主要是使用 NSObject 相关的方法 retain, release, dealloc 进行管理。
基本规则
- 自己创建的对象,自己持有
- 非自己创建的对象,也能持有
- 释放不再需要的某个对象
- 不能释放未持有的对象
自己创建的对象,自己持有
当使用以 alloc, new, copy, mutableCopy 开头的方法,创建对象时,持有该对象。
给某个对象发 retain 消息后,也能持有它
一般会在2种情况下使用 retain
- 在 init 方法中,将某个参数作为实例变量
- 避免某个对象因其他操作而被销毁。
使用 autorelease 来延迟发送 release 消息
- (NSString *)fullName {
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName] autorelease];
return string;
}
在上述代码中,因为 string 是由 alloc 方法生成的,所以你持有它,当方法结束后,你不再需要它,所以必须在方法结束前将其释放。
如果使用 release,那么在方法结束前,string 就被销毁了,根本无法返回。
所以只能使用 autorelease 来延迟释放它。
没有持有只是返回引用的对象
NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
// Deal with error...
}
// ...
[string release];
因为 error 不是你创建的,所以你没有持有它,也就不需要释放它。
覆写 dealloc 去释放持有的对象
不能直接调用 dealloc 方法。
在 dealloc 方法里,不要试图去释放稀有资源,如网络、缓存等。
在 MRC 环境,需要给实例变量发送 release 消息,最后需要调用 [super release]
。
Core Foundation 使用类似,但稍微不一样的规则。
内存管理详细介绍
使用 Accessor Methods 让内存管理更容易
Accessor Methods 即常说的 Getter 和 Setter 方法
'get' accessor
- (NSNumber *)count {
return _count;
}
'set' accessor
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// Make the new assignment.
_count = newCount;
}
如果你的类有一个属性是个对象,不妨假设为 P,它由另外一个对象 A 赋值得到,那么你必须保证 P 在使用过程中,A 不会被销毁。
所以你必须持有 A,并且在合适时机释放 A,但这很容易忘记,无疑会增加出问题的概率。
不要在 Initializer Methods 和 dealloc 中使用 Accessor Methods
正确的方式,应该像下面代码,直接赋值,不要使用 Setter 方法。
- init {
self = [super init];
if (self) {
_count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}
使用弱引用来避免循环引用
如果一个对象收到 retain 消息,那么将会有一个强引用指向它。
一个对象只有在没有任何强引用时,即引用计数为0,才能被销毁。
当2个对象直接或间接地强引用对方时,它们之间存在一个引用循环。
因为都存在强引用,除非在其中对象之一的内部,自动释放对另一对象的引用,否则两者都无法被销毁。
常见的情况就是使用 Block。
避免正在使用的对象被销毁
有些情况下,对象会被自动销毁
- 当从一个 collection 中移除时。
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.
当一个对象从 collection 中,比如 NSArray,被移除时,它会收到 release 消息,而不是 autorelease 消息,如果该 collection 是这个对象的唯一持有者,那么这个对象就会被销毁。
若想避免这种情况,需要对从 collection 中获取的对象,发送 retain 消息,这样就能持有它,当不需要时,再释放。
- 当『父对象』被销毁时
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
如上所示,对象 heisenObject 是从对象 parent 中获得,当 parent 被销毁时,如果 parent 是 heisenObject 的唯一持有者,那么 heisenObject 也会被销毁,相当于在 parent 的 dealloc 方法中,调用了 [heisenObject Release]
不要在 dealloc 中管理『稀有』资源
『稀有』资源有文件描述符、网络连接、缓冲、缓存等。
dealloc 何时被调用并不明确,有可能会被延时,也有可能是一步步执行的,甚至可能因为一个 bug 而造成应用闪退时,就被调用了。
collection 持有它们包含的对象
collection 有 array, dictionary, set 等等,如果一个对象被加入到 collection 时,该对象会调用 retain 方法,那么 collection 持有该对象。
当对象从 collection 中被移除时,该对象会被发送 release 消息。
持有规则的实现靠的是引用计数
- 当你创建一个对象时,它的引用计数为1。
- 当给一个对象发送 retain 消息时,它的引用计数加1。
- 当给一个对象发送 release 消息时,它的引用计数减1。
- 当给一个对象发送 autorelease 消息,它的引用计数会在当前 autorelease pool block 结束时减1。
- 当一个对象的引用计数为0时,它会被销毁。
使用 Autorelease Pool Blocks
@autoreleasepool {
// Code that creates autoreleased objects.
}
如上述代码所示
在 autorelease pool block 即将结束的时候,它当中那些收到过 autorelease 消息的对象,会被发送 release 消息。
即只要一个对象收到过 autorelease 消息,在当前 autorelease pool block 即将结束时,这个对象就会收到 release 消息。
autorelease pool block 可以互相嵌套,但比较少用。
Cocoa 希望代码都在一个 autorelease pool block 中,否则自动释放的对象不会被释放,这样就会有内存泄漏。
如果你在一个 autorelease pool block 外面发送 autorelease 消息,那么 Cocoa 将会报错。
AppKit 和 UIKit 的每次事件,事实上,都是运行在一个 autorelease pool block 中
事件指的是像一次点击这样的事件。
所以一般不需要自己创建一个 autorelease pool block,但也有一些情况例外:
- 如果你写的程序,不是基于 UI Framework 的,比如说 command-line tool。
- 如果你在每次循环里,创建了大量临时对象,那么最好在循环里创建一个 autorelease pool block,来降低应用的内存峰值。
- 如果你创建了多条线程,那么在线程开始时,你最好创建自己的 autorelease pool block。
在 Cocoa 应用中的每条线程,都包含独立的 autorelease pool block 栈,如果是多线程开发,务必创建自己的 autorelease pool block。