初始化方法内使用self有什么坏处?
场景描述
iOS初始化方法包括系统默认的和自定义的,常见系统初始化方法有init, initWithFrame:, initWithNibName:bundle:
等,自定义则是各式各样。日常iOS项目开发过程中,我们经常在类的初始化方法中初始化接下来类需要用到的一些必要的数据或界面。初始化方法内使用self
的场景大致有两种,一是self调用方法,诸如:[self doSomething]
,二是属性初始化,诸如:self.property = xxx
。样式大体如下:
@interface HHAnimal : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation HHAnimal
- (instancetype)init {
self = [super init];
if (self) {
self.name = @"HH";
[self doSomething];
}
return self;
}
- (void)doSomething {
}
@end
那么,为什么不建议在初始化方法内self.property = xxx or [self doSomething]
类似代码呢?
问题分析
当使用self.property = xxx
时,系统会帮我们做这两件事情:
- 方法调用。
[self setProperty:xxx]
- KVO。发送该属性的变化给监听者
那么,合并一下[self doSomething]
,初始化调用self
大体就分为方法调用和KVO。这两件事情在一般情况下不会有问题,但在类在初始化的过程中,类处于一种部分初始化的状态,此时很有可能出现错误。因为执行的方法体或者监听属性值变化的对象会认为当前执行过程是完全初始化的稳定状态,当类执行体使用了还未初始化的数据时,就可能发生数据错乱,程序异常或者crash。
下面我们举例子说明~
举例佐证
为了更好地说明,如下代码中,假设我们有一个HHAnimal
类,有三个属性,age
年龄,name
动物名字(目前是可读), attrDescription
表示用于展示的带颜色的名字(只读属性),它是一个计算变量---根据年龄变化,名字的颜色不一样。
@interface HHAnimal : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) NSAttributedString *attrDescription;
@end
@implementation HHAnimal
- (instancetype)init {
self = [super init];
if (self) {
self.age = 12;
_name = @"HH";
}
return self;
}
- (void)updateAttrDescription {
NSDictionary *attrs = nil;
if (self.age < 18) {
attrs = @{NSForegroundColorAttributeName: [UIColor greenColor]};
} else {
attrs = @{NSForegroundColorAttributeName: [UIColor yellowColor]};
}
_attrDescription = [[NSAttributedString alloc] initWithString:self.name attributes:attrs];
}
@end
我们从出现错误的概率,一级级的往上递增。现在我们希望在初始化后,attrDescription
变量也被初始化。第一种添加方法 (直接在设置完默认年龄后调用[self updateAttrDescription]
):
- (instancetype)init {
self = [super init];
if (self) {
self.age = 12;
[self updateAttrDescription];
_name = @"HH";
}
return self;
}
有人会说,这种方法弱爆了,看我的:
- (void)setAge:(NSUInteger)age {
_age = age;
[self updateAttrDescription];
}
这种方式高级一些,将attrDescription
跟年龄关联起来。嗯,不错。但还是crash了,因为在执行[self updateAttrDescription]
时,name
为nil,而[NSAttributedString initWithString:attributes:]
方法调用时若string为nil
,苹果爸爸直接给崩了。
那为什么你会这么写呢?其中原因之一可能是因为你不知道该系统方法不能将nil
作为参数,另外一个重要的点是你很明显看到name
在初始化方法中已经被赋值了,这样就不存在nil
的问题。
这种显而易见的场景在日常开发过程中,我们会很快发现。但,假设有一个子类HHHuman
继承了HHAnimal
,它只能看到父类的.h文件(且声明name
是具有初始值的)。若HHHuman
的实现内重写了age
的setter
方法,并将name
当做已初始化的一个变量使用的话,就可能引入崩溃等问题。
[UIViewController view]
除了上面介绍的一些例子,日常开发中一个更复杂也常见的例子要属UIViewController
的property--view
。假设在初始化的过程中写了self.view
,如下所示:
@implementation ViewController
- (instancetype)initWithOrderId:(NSString *)orderId {
self = [super init];
if (self) {
NSLog(@"%@", self.view);
_orderId = orderId;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[HHNetUtil requestWithOrderId:_orderId completionBlock:...];
}
这样写,会有什么奇怪的事情发生呢?正常的UIViewController
的初始化->界面展现流程是:
init -> loadView -> viewDidLoad -> viewWillApear:
调用self.view
后流程是:
init(loadView -> viewDidLoad) -> viewWillAppear:
在初始化中调用self.view
后,系统会自动触发loadView, viewDidLoad
流程。在init
方法<strong>期间</strong>会依次调用loadView -> viewDidLoad
,此时初始化的数据还未完成,viewDidLoad
方法很可能拿到空数据(比如上述代码根据init
初始化后的orderId
来请求订单相关数据),程序就会异常。除此之外,我们可能在创建完UIViewController
后,并不是想立即展现它,而是希望采用懒加载在想展示时,再进行viewDidLoad
过程中创建界面、数据处理或请求资源。
举了不少例子说明不宜在初始化中使用self
,那么还有方法需要注意吗?
dealloc内最好也别用self.property = xxx
跟初始化类似,dealloc
方法也是一个过程性,“不稳定”的方法。这里的不稳定指的是当前过程是一个不完全的状态,不完全初始化,不完全释放(析构)。
dealloc
除了会遇到初始化中介绍的问题以外,还经常出现KVO机制引发的异常。当一个对象A监听对象B的属性C时,如果在B的dealloc
内调用B.C = nil
,就会触发A中的监听方法。此时如果再使用B中的一些属性或者方法,B处于半释放状态,就会引起一些异常的奇奇怪怪的问题。所以,此时使用_C = nil
更加安全。
结语
本文先分析了不建议在初始化中使用self
的原因,并通过多个例子进行证明,最后衍生出dealloc
也最好别用的推论。虽然大多情况下,大家使用self
没有出错(在你一直都能保证调用的方法及属性的设置不会影响其他代码情况下),但风险就在那里,self
在,它就在。
说到这里,大家很可能会想到Objective-C
的继承者---Swift
类的两段式构造过程,它更安全、规范。Swift
通过这两个构造过程保证了所有需要初始化的属性都能初始化完,避免了因属性没有初始值导致之后使用过程不可预知状况。不太清楚又想了解Swift
类的两段式构造过程的同学可以戳官方中文教程。
最后,感谢大家的阅读,有问题请指正,大家相互讨论~
参考文章
Initializing a property, dot notation
Should I refer to self.property in the init method with ARC?
Objective-C init: Why It’s Helpful to Avoid Messages to self
Practical Memory Management