1.在delloc方法中只释放引用并解除监听
1.1在delloc方法中释放对象所拥有的引用并解除监听
(1)ARC会通过自动生成的.cxx_destruct方法在delloc中添加释放代码。对象所拥有的其他非Objective-C对象也要释放。比如CoreFoundation对象。
(2)在delloc要把原来配置过的观测行为都清理。如果用NSNotification给此对象订阅过某种通知,那么一般应该在注销,这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。
1.2开销较大或系统稀缺资源应该在delloc前释放:比如文件描述符、套接字、大块内存
原因如下:
(1)不能指望dealloc方法必定在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。如果非要等到系统调用dealloc方法时才释放,那么保留这些稀缺资源的时间有些长了。
(2)系统并保证每个创建出来的对象的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到dealloc消息。
通常的做法是,实现另外一些清理的方法,当应用程序用完资源对象后,就调用此方法。比如,如果某对象管理着连接服务器所用的套接字,此对象可能要通过套接字连接数据库。对于对象所属的类,其接口:
@interface
EOCServerConnection :
NSObject
- ( void )open:( NSString *)address;
- ( void )close;
- ( void )open:( NSString *)address;
- ( void )close;
@end
在dealloc中也要调用“清理方法”,以防开发者忘了清理这些资源。
- (
void
)close {
_close = YES ;
}
- ( void )dealloc {
if (!_close) {
[ self close];
}
_close = YES ;
}
- ( void )dealloc {
if (!_close) {
[ self close];
}
}
1.3不要在dealloc方法里随便调用其他方法。
无论在这里调用什么方法都不太应该,因为对象此时已经接近尾声了。如果在这里所调用的方法又要异步执行某些任务,当这些任务执行完后,要回调此对象,告诉对象任务已完成,而此时如果对象已摧毁,那么回调就会出错。
在dealloc里也不要调用属性的存取方法,因为这些方法会覆写,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于“键值观测”(KVO)机制的监控之下,该属性的观察者可能会在属性值改变时“保留”或使用这个即将回收的对象。
2.编写“异常安全代码”时留意内存管理问题
异常安全:在发生异常时,不会导致内存泄露。
2.1手动管理引用计数时异常安全
@try
{
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object soSomethingThatMayThrow];
[object release];
}
@catch (NSException *exception) {
NSLog ( @"Whoops, there was an error. Oh well..." );
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object soSomethingThatMayThrow];
[object release];
}
@catch (NSException *exception) {
NSLog ( @"Whoops, there was an error. Oh well..." );
}
如果
soSomethingThatMayThrow抛出异常,异常会令过程终止并跳转至catch块,因而其后的那行release代码不会运行。解决办法释放使用@finally,无论是否抛出异常,其中代码都保证会运行,且只运行一次。
EOCSomeClass *object
@try {
object = [[EOCSomeClass alloc] init];
[object soSomethingThatMayThrow];
}
@catch (NSException *exception) {
NSLog( @"Whoops, there was an error. Oh well..." );
}
@finally {
[object release];
@try {
object = [[EOCSomeClass alloc] init];
[object soSomethingThatMayThrow];
}
@catch (NSException *exception) {
NSLog( @"Whoops, there was an error. Oh well..." );
}
@finally {
[object release];
}
2.2ARC环境下异常安全
由于不能调用release,所以无法像手动管理引用计数时那样把释放操作移到@finally块中。若使用ARC且必须捕获异常,则需要打开编译器的-fobjc-arc-exceptions标志。ARC默认情况下是没有实现异常安全的,其原因如下:
1.ARC实现异常安全需要加入大量样板代码,以便跟踪清理的对象,从而在在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便不抛出异常也是如此。而且,添加进来的额外代码还会明显增加应用程序的大小。
2.Objective-C代码中,只有当应用程序因异常状况而终止时才抛出异常。
3.以“自动释放池块”降低内存峰值
释放对象有两种方式:一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入“自动释放池”中,自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送release消息。
一般情况下,无须担心自动释放池的创建问题。系统会自动为每个线程创建一个默认的自动释放池,每次执行“事件循环”时,就将其清空。
创建自动释放池的语法如下:
@autoreleasepool {
//...
}
自动释放池于左花括号处创建,并于右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。自动释放池机制就像“栈”一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值。
NSArray
*databaseRecords =
/* ... */
NSMutableArray *people = [NSMutableArray new];
for ( NSDictionary *record in databaseRecords) {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
NSMutableArray *people = [NSMutableArray new];
for ( NSDictionary *record in databaseRecords) {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
EOCPerson的初始化函数也许会再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。
NSArray
*databaseRecords =
/* ... */
NSMutableArray *people = [NSMutableArray new];
for ( NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
NSMutableArray *people = [NSMutableArray new];
for ( NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
如果把循环内代码包裹在“自动释放池块”中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。新增的自动释放池可以减少内存峰值,因为系统会在块的末尾把某些对象回收掉。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没有完成这一步,那就别着急优化。尽管自动释放池的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
4.“用僵尸对象”调试内存管理
向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。
Cocoa提供了“僵尸对象”来调试内存管理问题。启用这项调试功能之后,
运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会正在回收它们。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。
设置NSZombieEnbled后:
void
PrintClassInfo(
id
obj) {
Class cls = object_getClass(obj);
Class superCls = class_getSuperclass(cls);
NSLog( @"=== %s : %s ===" , class_getName(cls), class_getName(superCls));
}
int main( int argc, char *argc) {
EOCClass *obj = [[ EOCClass alloc ] init ];
NSLog ( @"Before release" );
PrintClassInfo (obj); // === EOCClass : NSObject ===
[obj release ];
NSLog ( @"After release" );
PrintClassInfo (obj); // === _NSZombile_EOCClass : nil ===
Class cls = object_getClass(obj);
Class superCls = class_getSuperclass(cls);
NSLog( @"=== %s : %s ===" , class_getName(cls), class_getName(superCls));
}
int main( int argc, char *argc) {
EOCClass *obj = [[ EOCClass alloc ] init ];
NSLog ( @"Before release" );
PrintClassInfo (obj); // === EOCClass : NSObject ===
[obj release ];
NSLog ( @"After release" );
PrintClassInfo (obj); // === _NSZombile_EOCClass : nil ===
}
对象所属的类已由EOCClass变为_NSZombie_EOCClass。_NSZombie_EOCClass实际上是在运行期生成的,当首次碰到EOCClass类的对象要变成僵尸对象时,就会创建这个类。创建过程如下:
//
获取回收对象所属的类名
const char *clsName = object_getClassName ( self );
// 生成僵尸对象名
const char *zombieClsName = "_ZSZombie_" + clsName;
// 判断这个僵尸对象是否已经存在
Class zombieCls = object_lookUpClass(zombieClsName);
// 如果不存在就创建
if (!zombieCls) {
// 获得僵尸对象 _NSZombie_ 模板
Class baseZobieCls = objc_lookUpClass( "_NSZombie_" );
zombieCls = object_duplicateClass(baseZobieCls,zombieClsName);
}
// 回收对象
objc_destructInstance( self );
const char *clsName = object_getClassName ( self );
// 生成僵尸对象名
const char *zombieClsName = "_ZSZombie_" + clsName;
// 判断这个僵尸对象是否已经存在
Class zombieCls = object_lookUpClass(zombieClsName);
// 如果不存在就创建
if (!zombieCls) {
// 获得僵尸对象 _NSZombie_ 模板
Class baseZobieCls = objc_lookUpClass( "_NSZombie_" );
zombieCls = object_duplicateClass(baseZobieCls,zombieClsName);
}
// 回收对象
objc_destructInstance( self );
object_setClass(self,zombieCls);
这个过程是NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把dealloc方法“调配”成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变成_NSZombie_OriginalClass了。
僵尸类是从名为_NSZombie_的模板类里复制出来的。创建新类的工作由运行期函数objc_duplicationClass( )来完成,它会把整个_NSZombie_类结构拷贝一份,并赋予其新的名字。副本的超类、实例变量以及方法都和复制前相同。
_NSZombie_类是类似于NSObject的根类,并未实现任何方法,该类只有一个实例变量,叫做isa。
由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制”。在完整的消息转发机制中,___forwarding___是核心,它首先要做的事就包括检查接收消息的对象所属的类名。若名称前缀为_NSZombie_,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就中止了。
虽说内存泄露了,但这只是调试手段,制作正式发行的应用程序时不会把这项功能打开,所以这种泄露无关要紧。