初始化方法內使用self有什么壞處?
場景描述
iOS初始化方法包括系統(tǒng)默認的和自定義的艰亮,常見系統(tǒng)初始化方法有init, initWithFrame:, initWithNibName:bundle:
等闭翩,自定義則是各式各樣。日常iOS項目開發(fā)過程中迄埃,我們經(jīng)常在類的初始化方法中初始化接下來類需要用到的一些必要的數(shù)據(jù)或界面疗韵。初始化方法內使用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
時驹马,系統(tǒng)會幫我們做這兩件事情:
- 方法調用革砸。
[self setProperty:xxx]
- KVO。發(fā)送該屬性的變化給監(jiān)聽者
那么窥翩,合并一下[self doSomething]
业岁,初始化調用self
大體就分為方法調用和KVO。這兩件事情在一般情況下不會有問題寇蚊,但在類在初始化的過程中笔时,類處于一種部分初始化的狀態(tài),此時很有可能出現(xiàn)錯誤仗岸。因為執(zhí)行的方法體或者監(jiān)聽屬性值變化的對象會認為當前執(zhí)行過程是完全初始化的穩(wěn)定狀態(tài)允耿,當類執(zhí)行體使用了還未初始化的數(shù)據(jù)時,就可能發(fā)生數(shù)據(jù)錯亂扒怖,程序異辰衔或者crash。
下面我們舉例子說明~
舉例佐證
為了更好地說明盗痒,如下代碼中蚂蕴,假設我們有一個HHAnimal
類,有三個屬性俯邓,age
年齡骡楼,name
動物名字(目前是可讀), attrDescription
表示用于展示的帶顏色的名字(只讀屬性),它是一個計算變量---根據(jù)年齡變化稽鞭,名字的顏色不一樣鸟整。
@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
我們從出現(xiàn)錯誤的概率,一級級的往上遞增‰蹋現(xiàn)在我們希望在初始化后篮条,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
跟年齡關聯(lián)起來琴拧。嗯降瞳,不錯。但還是crash了,因為在執(zhí)行[self updateAttrDescription]
時挣饥,name
為nil除师,而[NSAttributedString initWithString:attributes:]
方法調用時若string為nil
,蘋果爸爸直接給崩了扔枫。
那為什么你會這么寫呢汛聚?其中原因之一可能是因為你不知道該系統(tǒng)方法不能將nil
作為參數(shù),另外一個重要的點是你很明顯看到name
在初始化方法中已經(jīng)被賦值了短荐,這樣就不存在nil
的問題倚舀。
這種顯而易見的場景在日常開發(fā)過程中,我們會很快發(fā)現(xiàn)忍宋。但痕貌,假設有一個子類HHHuman
繼承了HHAnimal
,它只能看到父類的.h文件(且聲明name
是具有初始值的)糠排。若HHHuman
的實現(xiàn)內重寫了age
的setter
方法舵稠,并將name
當做已初始化的一個變量使用的話,就可能引入崩潰等問題入宦。
[UIViewController view]
除了上面介紹的一些例子哺徊,日常開發(fā)中一個更復雜也常見的例子要屬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:...];
}
這樣寫落追,會有什么奇怪的事情發(fā)生呢?正常的UIViewController
的初始化->界面展現(xiàn)流程是:
init -> loadView -> viewDidLoad -> viewWillApear:
調用self.view
后流程是:
init(loadView -> viewDidLoad) -> viewWillAppear:
在初始化中調用self.view
后涯肩,系統(tǒng)會自動觸發(fā)loadView, viewDidLoad
流程轿钠。在init
方法<strong>期間</strong>會依次調用loadView -> viewDidLoad
,此時初始化的數(shù)據(jù)還未完成病苗,viewDidLoad
方法很可能拿到空數(shù)據(jù)(比如上述代碼根據(jù)init
初始化后的orderId
來請求訂單相關數(shù)據(jù))谣膳,程序就會異常。除此之外铅乡,我們可能在創(chuàng)建完UIViewController
后,并不是想立即展現(xiàn)它烈菌,而是希望采用懶加載在想展示時阵幸,再進行viewDidLoad
過程中創(chuàng)建界面、數(shù)據(jù)處理或請求資源芽世。
舉了不少例子說明不宜在初始化中使用self
挚赊,那么還有方法需要注意嗎?
dealloc內最好也別用self.property = xxx
跟初始化類似济瓢,dealloc
方法也是一個過程性荠割,“不穩(wěn)定”的方法。這里的不穩(wěn)定指的是當前過程是一個不完全的狀態(tài),不完全初始化蔑鹦,不完全釋放(析構)夺克。
dealloc
除了會遇到初始化中介紹的問題以外,還經(jīng)常出現(xiàn)KVO機制引發(fā)的異常嚎朽。當一個對象A監(jiān)聽對象B的屬性C時铺纽,如果在B的dealloc
內調用B.C = nil
,就會觸發(fā)A中的監(jiān)聽方法哟忍。此時如果再使用B中的一些屬性或者方法狡门,B處于半釋放狀態(tài),就會引起一些異常的奇奇怪怪的問題锅很。所以其馏,此時使用_C = nil
更加安全。
結語
本文先分析了不建議在初始化中使用self
的原因爆安,并通過多個例子進行證明叛复,最后衍生出dealloc
也最好別用的推論。雖然大多情況下鹏控,大家使用self
沒有出錯(在你一直都能保證調用的方法及屬性的設置不會影響其他代碼情況下)致扯,但風險就在那里,self
在当辐,它就在抖僵。
說到這里,大家很可能會想到Objective-C
的繼承者---Swift
類的兩段式構造過程缘揪,它更安全耍群、規(guī)范。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