偶然間看到「 Designated Initializer」一詞,內心一驚,這是什麼東西,怎麼沒據說過?難道是我道行太淺?真的是這樣?(好傷心啊)一陣子我懷疑以後,果斷上網查了一下這個 Designated Initializer,發現名詞新穎,可是這用法倒是用了無數遍啊,SO?記錄一下吧,下次再看到至少知道這麼個高端大氣的詞啊,不能讓別人覺得咱是個鄉巴佬啊😂(雖然事實倒是如此,可是也不能認可)html
1、iOS的對象建立和初始化
iOS 中對象建立是分兩步完成:
咱們最熟悉的建立NSObject對象的過程:
蘋果官方有一副圖片更生動的描述了這個過程:
對象的初始化是一個很重要的過程,一般在初始化的時候咱們會支持成員變量的初始狀態,建立關聯的對象等。例如對於以下對象:

Test ViewController
上面的VC中有一個成員變量XXService,在viewWillAppear的時候發起網絡請求獲取數據填充VC。
你們以爲上面的代碼有沒有什麼問題?
帶着這個問題咱們繼續往下看,上面只有VC的實現代碼,VC經過什麼姿式建立,咱們不得而知,下面分兩種狀況:
1. 手動建立
一般爲了省事,咱們建立VC的時候常用以下方式
ViewController *vc =
[ViewController alloc] init];
ViewController *vc = [ViewController alloc] initWithNibName:nil bundle:nil];
使用如上兩種方式建立,咱們上面的那一段代碼均可以正常運行,由於成員變量_service被正確的初始化了。
2. 從storyboard加載或者反序列化而來
先來看一段蘋果官方的文案:
When using a storyboard to define your view controller and its associated views, you never initialize your view controller class directly. Instead, view controllers are instantiated by the storyboard either automatically when a segue is triggered or programmatically when your app calls the instantiateViewControllerWithIdentifier: method of a storyboard object. When instantiating a view controller from a storyboard, iOS initializes the new view controller by calling its initWithCoder: method instead of this method and sets the nibName property to a nib file stored inside the storyboard.
從Xcode5之後建立新的工程默認都是Storyboard的方式管理和加載VC,對象的初始化壓根不會調用 initWithNibName:bundle: 方法,而是調用了 initWithCoder: 方法。對照上面VC的實現,能夠看出_service對象沒有被正確初始化,因此請求沒法發出。
至此第一個問題你們心中應該已經有了答案,下面讓咱們再去看看問題背後的更深層的緣由。
正確的運行結果並不表明正確的執行邏輯,有時候可能正好是巧合而已
2、Designated Initializer (指定初始化函數)
在UIViewController的頭文件中咱們能夠看到以下兩個初始化方法:
|
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
|
細心的同窗可能已經發現了一個宏 「
NS_DESIGNATED_INITIALIZER」, 這個宏定義在NSObjCRuntime.h這個頭文件中,定義以下
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
"__has_attribute"是Clang 的一個用於檢測當前編譯器是否支持某一特性的一個宏,對你沒有聽錯,"__has_attribute" 也是一個宏。詳細信息見: Type Safety Checking
經過上面的定義,咱們能夠看到"NS_DESIGNATED_INITIALIZER"實際上是給初始化函數聲明的後面加上了一個編譯器可見的標記,不要小看這個標記,他能夠在編譯時就幫咱們找出一些潛在的問題,避免程序運行時出現一些奇奇怪怪的行爲。
聽着神乎其神,編譯器怎麼幫咱們避免呢?
答案是:⚠⚠⚠警告
以下圖:
編譯器出現警告,說明咱們寫的代碼不夠規範。Xcode自帶的Analytics工具能夠幫助咱們找出程序的潛在的問題,多花點時間規範本身的代碼,消除項目中的警告,避免後面項目上線後出現奇奇怪怪的問題。
3、NS_DESIGNATED_INITIALIZER 正確使用姿式是什麼?
指定初始化函數 Vs 便利初始化函數
指定初始化函數對一個類來講很是重要,一般參數也是最多的,試想每次咱們須要建立一個自定義類都須要一堆參數,那豈不是很痛苦。便利初始化函數就是用來幫咱們解決這個問題的,可讓咱們比較的建立對象,同時又能夠保證類的成員變量被設置爲默認的值。
不過須要注意,爲了享受這些「便利」,咱們須要遵照一些規範,官方文檔連接以下:
Swift和Objective-C略有不一樣,下面咱們以Objective-C的規範爲例。
1. 子類若是有指定初始化函數,那麼指定初始化函數實現時必須調用它的直接父類的指定初始化函數。
2. 若是子類有指定初始化函數,那麼便利初始化函數必須調用本身的其它初始化函數(包括指定初始化函數以及其餘的便利初始化函數),不能調用super的初始化函數。
基於第2條的定義咱們能夠推斷出:全部的便利初始化函數最終都會調到該類的指定初始化函數
緣由:全部的便利初始化函數必須調用的其餘初始化函數,若是程序可以正常運行,那麼必定不會出現直接遞歸,或者間接遞歸的狀況。那麼假設一個類有指定函數A,便利初始化函數B,C,D,那麼B,C,D三者之間不管怎麼調用總的有一我的打破這個循環,那麼一定會有一個調用指向了A,從而其餘兩個也最終會指向A。
示意圖以下(圖畫的比較醜,你們明白意思就好):
3. 若是子類提供了指定初始化函數,那麼必定要實現全部父類的指定初始化函數。
當子類定義了本身的指定初始化函數以後,父類的指定初始化函數就「退化」爲子類的便利初始化函數。這一條規範的目的是: 「保證子類新增的變量可以被正確初始化。」
由於咱們無法限制使用者經過什麼什麼方式建立子類,例如咱們在建立UIViewController的時候可使用以下三種方式:
UIViewController *vc =
[[UIViewController alloc] init];
UIViewController *vc = [[UIViewController alloc] initWithNibName:nil bundle:nil];
UIViewController *vc = [[UIViewController alloc] initWithCoder:xxx];
若是子類沒有重寫父類的全部初始化函數,而使用者剛好直接使用父類的初始化函數初始化對象,那麼子類的成員變量就可能存在沒有正確初始化的狀況。
4、舉個栗子
以上三條規範理解起來可能有點兒繞,我寫了個簡單的例子有助於理解該規範,代碼以下:
@interface Animal : NSObject {
NSString *_name;
}
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
@end
@implementation Animal
- (instancetype)initWithName:(NSString *)name
{
self = [super init];
if (self) {
_name = name;
}
return self;
}
- (instancetype)init
{
return [self initWithName:@"Animal"];
}
@end
@interface Mammal : Animal {
NSInteger _numberOfLegs;
}
- (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithLegs:(NSInteger)numberOfLegs;
@end
@implementation Mammal
- (instancetype)initWithLegs:(NSInteger)numberOfLegs
{
self = [self initWithName:@"Mammal"];
if (self) {
_numberOfLegs = numberOfLegs;
}
return self;
}
- (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs
{
self = [super initWithName:name];
if (self) {
_numberOfLegs = numberOfLegs;
}
return self;
}
- (instancetype)initWithName:(NSString *)name
{
return [self initWithName:name andLegs:4];
}
@end
@interface Whale : Mammal {
BOOL _canSwim;
}
- (instancetype)initWhale NS_DESIGNATED_INITIALIZER;
@end
@implementation Whale
- (instancetype)initWhale
{
self = [super initWithName:@"Whale" andLegs:0];
if (self) {
_canSwim = YES;
}
return self;
}
- (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs
{
return [self initWhale];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"Name: %@, Numberof Legs %zd, CanSwim %@", _name, _numberOfLegs, _canSwim ? @"YES" : @"NO"];
}
@end
TestDesignatedInitializer
配套上面的代碼,我還畫了一張類調用圖幫助你們理解,以下:
咱們聲明瞭三個類:Animal(動物),Mammal(哺乳動物),Whale(鯨魚),而且按照指定初始化函數的規範實現了全部的初始化函數。
下面咱們建立一些Whale(鯨魚),測試一下健壯性,代碼以下:
Whale *whale1 = [[Whale alloc] initWhale]; // 1
NSLog(@"whale1 %@", whale1);
Whale *whale2 = [[Whale alloc] initWithName:@"Whale"]; // 2
NSLog(@"whale2 %@", whale2);
Whale *whale3 = [[Whale alloc] init]; // 3
NSLog(@"whale3 %@", whale3);
Whale *whale4 = [[Whale alloc] initWithLegs:4]; // 4
NSLog(@"whale4 %@", whale4);
Whale *whale5 = [[Whale alloc] initWithName:@"Whale" andLegs:8]; // 5
NSLog(@"whale5 %@", whale5);
執行結果爲:
whale1 Name: Whale, Numberof Legs
0, CanSwim YES
whale2 Name: Whale, Numberof Legs 0, CanSwim YES
whale3 Name: Whale, Numberof Legs 0, CanSwim YES
whale4 Name: Whale, Numberof Legs 4, CanSwim YES
whale5 Name: Whale, Numberof Legs 0, CanSwim YES
分析能夠得出:
whale1 使用 Whale 的指定初始化函數建立,初始化調用順序爲: ⑧ -> ⑤ -> ③ -> ①,初始化方法的實際執行順序剛好相反: ① -> ③ -> ⑤ -> ⑧,即從根類的開始初始化,初始化的順序正好和類成員變量的佈局順序相同,有興趣的能夠自行上網查查。
whale5 使用Whale的父類Mammal的指定初始化函數建立實例,初始化調用順序爲: ⑦ -> ⑧ -> ⑤ -> ③ -> ①,建立出來的對象符合預期。
注:⑦ 表明 Whale 類的實現,其內部實現調用了本身類的指定初始化函數 initWhale。 ⑤ 表明 Mammal 類的實現。
細心地朋友可能已經發咱們建立的第四條鯨魚,神奇的長了4條腿,讓咱們看看建立過程的調用順序: ⑥ -> ④ -> ⑦ -> ⑧ -> ⑤ -> ③ -> ①, 能夠看到對象的初始化也是徹底從跟到當前類的順序依次初始化的,那麼問題出在哪兒呢?
Mammal 類的 initWithLegs:函數,除了正常的初始化函數調用棧,它還一段函數體,對已經初始化好的對象的成員變量_numberOfLegs 從新設置了值,這就致使了鯨魚長出了4條腿。
- (instancetype)initWithLegs:(NSInteger)numberOfLegs
{
self = [self initWithName:@"Mammal"];
if (self) {
_numberOfLegs = numberOfLegs;
}
return self;
}
細心的同窗會發現,不管你使用父類的仍是爺爺類的初始化函數建立子類的對象,最後四個調用順序都爲:⑧ -> ⑤ -> ③ -> ①。
指定初始化函數規則只能用來保證對象的建立過程是從跟類到子類依次初始化全部成員變量,沒法解決業務問題。
5、當 initWithCoder: 遇到 NS_DESIGNATED_INITIALIZER
NSCoding協議的定義以下:
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end
In the implementation of an initWithCoder: method, the object should first invoke its superclass’s designated initializer to initialize inherited state, and then it should decode and initialize its state. If the superclass adopts the NSCoding protocol, you start by assigning of the return value of initWithCoder: to self.
翻譯一下:
- 如父類沒有實現NSCoding協議,那麼應該調用父類的指定初始化函數。
- 若是父類實現了NSCoing協議,那麼子類的 initWithCoder: 的實現中須要調用父類的initWithCoder:方法,
根據上面的第三部分闡述的指定初始化函數的三個規則,而NSCoding實現的兩個原則都須要父類的初始化函數,這違反了指定初始化實現的第二條原則。
怎麼辦?
仔細觀察NSCoding協議中 initWithCoder: 的定義後面有一個註釋掉的 NS_DESIGNATED_INITIALIZER,是否是能夠找到一點兒靈感呢!
實現NSCoding協議的時候,咱們能夠顯示的聲明 initWithCoder: 爲指定初始化函數(一個類能夠有多個指定初始化函數,好比UIViewController)便可完美解決問題,既知足了指定初始化函數的三個規則,又知足了NSCoding協議的三條原則。
6、總結
上面關於指定初始化的規則講了那麼多,其實能夠概括爲兩點:
- 便利初始化函數只能調用本身類中的其餘初始化方法
- 指定初始化函數纔有資格調用父類的指定初始化函數
蘋果官方有個圖,有助於咱們理解這兩點:
當咱們爲本身建立的類添加指定初始化函數時,必須準確的識別並覆蓋直接父類全部的指定初始化函數,這樣才能保證整個子類的初始化過程能夠覆蓋到全部繼承鏈上的成員變量獲得合適的初始化。
NS_DESIGNATED_INITIALIZER 是一個頗有用的宏,充分發揮編譯器的特性幫咱們找出初始化過程當中可能存在的漏洞,加強代碼的健壯性。
參考資料: