大数据

内存管理-dealloc方法到底应该怎么写?

前言

使用ARC已经很长时间了,基本已经快忘却了retain、release、dealloc等方法了,但即便使用ARC,对于一些内存的处理我们依然需要手动进行。比如dealloc方法,当我们重载dealloc方法一样会被调用,只是不能调用其父类的方法[super dealloc],在dealloc方法中通常需要做的有移除通知或监听操作,或对于一些非Objective-C对象也需要手动清空,比如CoreFoundation中的对象。再而ARC的内存销毁具有一定的滞后性,也可将一些变量手动置空,也就是告诉系统这些变量已经使用完毕可以释放了,但是对于变量置空一直有这样的疑惑,下面两种写法到底有什么不同?

- (void)dealloc {
    self.name = nil;
}
- (void)dealloc {
    _name = nil;
}

翻阅了苹果的官方文档Don’t Use Accessor Methods in Initializer Methods and dealloc,但其只是简单的说明不要在init与dealloc方法中使用读写方法进行初始化设置,并未详细的说明其原因,遂查阅了网上的一些问答,在这里加以总结,该文由此而来。

正文

首先认知如上两种写法的区别,self.name意为调用属性name的setter方法进行赋值

- (void)setName:(NSString*)name {
    _name = name;
}

即将nil作为参数传递到setName:方法中,其方法内部本身执行的仍为_name = nil,乍看来和手动在dealloc中书写_name = nil没有什么区别,但是setter方法并没有看起那样简单,若在MRC中通常setter方法如下所示:

- (void)setName:(NSString*)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

对于setter赋值方法采用的为释放旧值保留新值的方式。直接调用_name=nil避免了指针转移问题且避免了Objcetive-C的“方法派发”操作,因此直接调用_name=nil相比self.name=nil执行效率会高一些。

参考自:《Effective Objective-C 2.0》第7条:在对象内部尽量直接访问实例变量。

难道仅仅是提升了一些肉眼不能察觉的执行效率提升问题吗?肯定不是的,在某些情况下如果使用self.name=nil这种写法会使程序造成错误甚至崩溃。

假设在父类的dealloc方法中使用读写方法(self.name=nil)进行置空操作,如果子类重写了其读写方法,当所创建的子类对象销毁时进而调用父类的dealloc方法,就会造成访问已释放对象的情况,从而发生崩溃,口述不明显,代码如下:

父类
#import 
@interface BaseClass : NSObject

@property(nonatomic) NSString *baseObj;

@end
#import "BaseClass.h"

@implementation BaseClass

- (void)dealloc {
    self.baseObj = nil;//读写方法置空
}

@end
子类
#import "BaseClass.h"

@interface SubClass : BaseClass

@property (nonatomic) NSString *subObj;

@end
#import "SubClass.h"

@implementation SubClass

- (void)setBaseObj:(NSString *)baseObj {
    NSLog(@"%@",[NSString stringWithString:_subObj]);
}

- (void)dealloc {
    _subObj = nil;
}

例中在BaseClass中声明属性baseObj,子类SubClass继承于BaseClass并重写其属性baseObj的setter方法,并在此打印了本类的属性self.subObj。

在主函数中运行如下代码会出现什么情况?

- (void)viewDidLoad {
    [super viewDidLoad];

    SubClass *class = [[SubClass alloc]init];
    class.subObj = @"子类属性";
}

没错,程序崩溃了,那我们分析下为什么会产生崩溃情况。我们可以分别在父类与子类的dealloc方法和已经重写的setBaseObj方法中添加断点查看运行流程。

执行顺序为

子类dealloc方法

父类dealloc方法

重写的父类baseObj的setter方法

简单分析下,当SubClass所创建的对象使用完毕销毁会调用其本身dealloc方法,接下来dealloc内部调用[super dealloc]从而执行父类dealloc方法,但因为父类dealloc方法是使用self.baseObj=nil这种读写方法进行置空,因此接下来调用子类重写的setBaseObj方法,但此时子类已经销毁,访问并使用了一个已经不存在的对象,从而发生崩溃。

因此说明在dealloc中使用读取方法置空是不安全的,轻则获取null,重则程序崩溃。

参考自:不要在init和dealloc函数中使用accessor

最后我们在回想下MRC中dealloc的写法

- (void)dealloc {
    [_name release];
    _name = nil;
}

在MRC下应该使用[_name release]还是[self.name release],或者在ARC下系统又是如何处理的呢?

当然是采用[_name release]而非[self.name release],因为我们调用的getter方法本质是这样的形式:

- (NSString*)name {
   return [[_name retain] autorelease];
}

可以看出[self.name release]添加了额外的retain与autorelease操作,释放时机无法控制,可能会导致重复释放导致程序崩溃,而通过[_name release]可以直接将该变量的内存进行释放,避免崩溃或悬空指针,因此还是[_name release]会更好些。

参考自:Why shouldn’t I use the getter to release a property in objective-c?

该文对总结的问答进行整理以加深印象,如有其他见解欢迎评论指出。