iOS:聊聊Designated Initializer(指定初始化函數(shù))
一、iOS的對(duì)象創(chuàng)建和初始化
iOS 中對(duì)象創(chuàng)建是分兩步完成:
分配內(nèi)存
初始化對(duì)象的成員變量
我們最熟悉的創(chuàng)建NSObject對(duì)象的過程:
蘋果官方有一副圖片更生動(dòng)的描述了這個(gè)過程:
對(duì)象的初始化是一個(gè)很重要的過程,通常在初始化的時(shí)候我們會(huì)支持成員變量的初始狀態(tài),創(chuàng)建關(guān)聯(lián)的對(duì)象等。例如對(duì)于如下對(duì)象:
- @interface ViewController : UIViewController
- @end
- @interface ViewController () {
- XXService *_service;
- }
- @end
- @implementation ViewController
- - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
- {
- self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
- if (self) {
- _service = [[XXService alloc] init];
- }
- return self;
- }
- - (void)viewWillAppear:(BOOL)animated
- {
- [super viewWillAppear:animated];
- [_service doRequest];
- }
- ...
- @end
- Test ViewController
面的VC中有一個(gè)成員變量XXService,在viewWillAppear的時(shí)候發(fā)起網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù)填充VC。
大家覺得上面的代碼有沒有什么問題?
帶著這個(gè)問題我們繼續(xù)往下看,上面只有VC的實(shí)現(xiàn)代碼,VC通過什么姿勢(shì)創(chuàng)建,我們不得而知,下面分兩種情況:
1. 手動(dòng)創(chuàng)建
通常為了省事,我們創(chuàng)建VC的時(shí)候經(jīng)常使用如下方式
- ViewController *vc = [ViewController alloc] init];
- ViewController *vc = [ViewController alloc] initWithNibName:nil bundle:nil];
使用如上兩種方式創(chuàng)建,我們上面的那一段代碼都可以正常運(yùn)行,因?yàn)槌蓡T變量_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以后創(chuàng)建新的工程默認(rèn)都是Storyboard的方式管理和加載VC,對(duì)象的初始化壓根不會(huì)調(diào)用 initWithNibName:bundle: 方法,而是調(diào)用了 initWithCoder: 方法。對(duì)照上面VC的實(shí)現(xiàn),可以看出_service對(duì)象沒有被正確初始化,所以請(qǐng)求無法發(fā)出。
至此***個(gè)問題大家心中應(yīng)該已經(jīng)有了答案,下面讓我們?cè)偃タ纯磫栴}背后的更深層的原因。
正確的運(yùn)行結(jié)果并不代表正確的執(zhí)行邏輯,有時(shí)候可能正好是巧合而已
二、Designated Initializer (指定初始化函數(shù))
在UIViewController的頭文件中我們可以看到如下兩個(gè)初始化方法:
- - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了一個(gè)宏 “NS_DESIGNATED_INITIALIZER”, 這個(gè)宏定義在NSObjCRuntime.h這個(gè)頭文件中,定義如下:
- #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 的一個(gè)用于檢測(cè)當(dāng)前編譯器是否支持某一特性的一個(gè)宏,對(duì)你沒有聽錯(cuò),"__has_attribute" 也是一個(gè)宏。
通過上面的定義,我們可以看到"NS_DESIGNATED_INITIALIZER"其實(shí)是給初始化函數(shù)聲明的后面加上了一個(gè)編譯器可見的標(biāo)記,不要小看這個(gè)標(biāo)記,他可以在編譯時(shí)就幫我們找出一些潛在的問題,避免程序運(yùn)行時(shí)出現(xiàn)一些奇奇怪怪的行為。
聽著神乎其神,編譯器怎么幫我們避免呢?
答案是:⚠️⚠️⚠️警告
如下圖:
編譯器出現(xiàn)警告,說明我們寫的代碼不夠規(guī)范。Xcode自帶的Analytics工具可以幫助我們找出程序的潛在的問題,多花點(diǎn)時(shí)間規(guī)范自己的代碼,消除項(xiàng)目中的警告,避免后面項(xiàng)目上線后出現(xiàn)奇奇怪怪的問題。
三、NS_DESIGNATED_INITIALIZER 正確使用姿勢(shì)是什么?
指定初始化函數(shù) Vs 便利初始化函數(shù)
指定初始化函數(shù)對(duì)一個(gè)類來說非常重要,通常參數(shù)也是最多的,試想每次我們需要?jiǎng)?chuàng)建一個(gè)自定義類都需要一堆參數(shù),那豈不是很痛苦。便利初始化函數(shù)就是用來幫我們解決這個(gè)問題的,可以讓我們比較的創(chuàng)建對(duì)象,同時(shí)又可以保證類的成員變量被設(shè)置為默認(rèn)的值。
不過需要注意,為了享受這些“便利”,我們需要遵守一些規(guī)范,官方文檔鏈接如下:
Swift和Objective-C略有不同,下面我們以O(shè)bjective-C的規(guī)范為例。
1. 子類如果有指定初始化函數(shù),那么指定初始化函數(shù)實(shí)現(xiàn)時(shí)必須調(diào)用它的直接父類的指定初始化函數(shù)。
2. 如果子類有指定初始化函數(shù),那么便利初始化函數(shù)必須調(diào)用自己的其它初始化函數(shù)(包括指定初始化函數(shù)以及其他的便利初始化函數(shù)),不能調(diào)用super的初始化函數(shù)。
基于第2條的定義我們可以推斷出:所有的便利初始化函數(shù)最終都會(huì)調(diào)到該類的指定初始化函數(shù)
原因:所有的便利初始化函數(shù)必須調(diào)用的其他初始化函數(shù),如果程序能夠正常運(yùn)行,那么一定不會(huì)出現(xiàn)直接遞歸,或者間接遞歸的情況。那么假設(shè)一個(gè)類有指定函數(shù)A,便利初始化函數(shù)B,C,D,那么B,C,D三者之間無論怎么調(diào)用總的有一個(gè)人打破這個(gè)循環(huán),那么必定會(huì)有一個(gè)調(diào)用指向了A,從而其他兩個(gè)也最終會(huì)指向A。
示意圖如下(圖畫的比較丑,大家明白意思就好):
3. 如果子類提供了指定初始化函數(shù),那么一定要實(shí)現(xiàn)所有父類的指定初始化函數(shù)。
當(dāng)子類定義了自己的指定初始化函數(shù)之后,父類的指定初始化函數(shù)就“退化”為子類的便利初始化函數(shù)。這一條規(guī)范的目的是: “保證子類新增的變量能夠被正確初始化。”
因?yàn)槲覀儧]法限制使用者通過什么什么方式創(chuàng)建子類,例如我們?cè)趧?chuàng)建UIViewController的時(shí)候可以使用如下三種方式:
- UIViewController *vc = [[UIViewController alloc] init];
- UIViewController *vc = [[UIViewController alloc] initWithNibName:nil bundle:nil];
- UIViewController *vc = [[UIViewController alloc] initWithCoder:xxx];
四、舉個(gè)栗子
以上三條規(guī)范理解起來可能有點(diǎn)兒繞,我寫了個(gè)簡(jiǎn)單的例子有助于理解該規(guī)范,代碼如下:
- @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
配套上面的代碼,我還畫了一張類調(diào)用圖幫助大家理解,如下:
我們聲明了三個(gè)類:Animal(動(dòng)物),Mammal(哺乳動(dòng)物),Whale(鯨魚),并且按照指定初始化函數(shù)的規(guī)范實(shí)現(xiàn)了所有的初始化函數(shù)。
下面我們創(chuàng)建一些Whale(鯨魚),測(cè)試一下健壯性,代碼如下:
- 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);
執(zhí)行結(jié)果為:
- 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 的指定初始化函數(shù)創(chuàng)建,初始化調(diào)用順序?yàn)? ⑧ -> ⑤ -> ③ -> ①,初始化方法的實(shí)際執(zhí)行順序恰好相反: ① -> ③ -> ⑤ -> ⑧,即從根類的開始初始化,初始化的順序正好和類成員變量的布局順序相同,有興趣的可以自行上網(wǎng)查查。
whale5 使用Whale的父類Mammal的指定初始化函數(shù)創(chuàng)建實(shí)例,初始化調(diào)用順序?yàn)? ⑦ -> ⑧ -> ⑤ -> ③ -> ①,創(chuàng)建出來的對(duì)象符合預(yù)期。
注:⑦ 代表 Whale 類的實(shí)現(xiàn),其內(nèi)部實(shí)現(xiàn)調(diào)用了自己類的指定初始化函數(shù) initWhale。 ⑤ 代表 Mammal 類的實(shí)現(xiàn)。
細(xì)心地朋友可能已經(jīng)發(fā)我們創(chuàng)建的第四條鯨魚,神奇的長(zhǎng)了4條腿,讓我們看看創(chuàng)建過程的調(diào)用順序: ⑥ -> ④ -> ⑦ -> ⑧ -> ⑤ -> ③ -> ①, 可以看到對(duì)象的初始化也是完全從跟到當(dāng)前類的順序依次初始化的,那么問題出在哪兒呢?
Mammal 類的 initWithLegs:函數(shù),除了正常的初始化函數(shù)調(diào)用棧,它還一段函數(shù)體,對(duì)已經(jīng)初始化好的對(duì)象的成員變量_numberOfLegs 重新設(shè)置了值,這就導(dǎo)致了鯨魚長(zhǎng)出了4條腿。
- - (instancetype)initWithLegs:(NSInteger)numberOfLegs
- {
- self = [self initWithName:@"Mammal"];
- if (self) {
- _numberOfLegs = numberOfLegs;
- }
- return self;
- }
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn),無論你使用父類的還是爺爺類的初始化函數(shù)創(chuàng)建子類的對(duì)象,***四個(gè)調(diào)用順序都為:⑧ -> ⑤ -> ③ -> ①。
指定初始化函數(shù)規(guī)則只能用來保證對(duì)象的創(chuàng)建過程是從跟類到子類依次初始化所有成員變量,無法解決業(yè)務(wù)問題。
五、當(dāng) initWithCoder: 遇到 NS_DESIGNATED_INITIALIZER
NSCoding協(xié)議的定義如下:
- @protocol NSCoding
- - (void)encodeWithCoder:(NSCoder *)aCoder;
- - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
- @end
蘋果官方文檔Decoding an Object中明確規(guī)定:
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. |
翻譯一下:
如父類沒有實(shí)現(xiàn)NSCoding協(xié)議,那么應(yīng)該調(diào)用父類的指定初始化函數(shù)。
如果父類實(shí)現(xiàn)了NSCoing協(xié)議,那么子類的 initWithCoder: 的實(shí)現(xiàn)中需要調(diào)用父類的initWithCoder:方法,
根據(jù)上面的第三部分闡述的指定初始化函數(shù)的三個(gè)規(guī)則,而NSCoding實(shí)現(xiàn)的兩個(gè)原則都需要父類的初始化函數(shù),這違反了指定初始化實(shí)現(xiàn)的第二條原則。
怎么辦?
仔細(xì)觀察NSCoding協(xié)議中 initWithCoder: 的定義后面有一個(gè)注釋掉的 NS_DESIGNATED_INITIALIZER,是不是可以找到一點(diǎn)兒靈感呢!
實(shí)現(xiàn)NSCoding協(xié)議的時(shí)候,我們可以顯示的聲明 initWithCoder: 為指定初始化函數(shù)(一個(gè)類可以有多個(gè)指定初始化函數(shù),比如UIViewController)即可***解決問題,既滿足了指定初始化函數(shù)的三個(gè)規(guī)則,又滿足了NSCoding協(xié)議的三條原則。
六、總結(jié)
上面關(guān)于指定初始化的規(guī)則講了那么多,其實(shí)可以歸納為兩點(diǎn):
- 便利初始化函數(shù)只能調(diào)用自己類中的其他初始化方法
- 指定初始化函數(shù)才有資格調(diào)用父類的指定初始化函數(shù)
蘋果官方有個(gè)圖,有助于我們理解這兩點(diǎn):
當(dāng)我們?yōu)樽约簞?chuàng)建的類添加指定初始化函數(shù)時(shí),必須準(zhǔn)確的識(shí)別并覆蓋直接父類所有的指定初始化函數(shù),這樣才能保證整個(gè)子類的初始化過程可以覆蓋到所有繼承鏈上的成員變量得到合適的初始化。
NS_DESIGNATED_INITIALIZER 是一個(gè)很有用的宏,充分發(fā)揮編譯器的特性幫我們找出初始化過程中可能存在的漏洞,增強(qiáng)代碼的健壯性。