iOS: 聊聊 Designated Initializer(指定初始化函數)


 

iOS: 聊聊 Designated Initializer(指定初始化函數)

 

一、iOS的對象創建和初始化

iOS 中對象創建是分兩步完成:

  • 分配內存
  • 初始化對象的成員變量

我們最熟悉的創建NSObject對象的過程:

蘋果官方有一副圖片更生動的描述了這個過程:

對象的初始化是一個很重要的過程,通常在初始化的時候我們會支持成員變量的初始狀態,創建關聯的對象等。例如對於如下對象:

 1 @interface ViewController : UIViewController
 2 
 3 @end
 4 
 5 
 6 @interface ViewController () {
 7     XXService      *_service;
 8 }
 9 
10 @end
11 
12 @implementation ViewController
13 
14 - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
15 {
16     self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
17     if (self) {
18         _service = [[XXService alloc] init];
19     }
20     
21     return self;
22 }
23 
24 - (void)viewWillAppear:(BOOL)animated
25 {
26     [super viewWillAppear:animated];
27     
28     [_service doRequest];
29 }
30 
31 ...
32 
33 @end
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對象沒有被正確初始化,所以請求無法發出。

至此第一個問題大家心中應該已經有了答案,下面讓我們再去看看問題背后的更深層的原因。

正確的運行結果並不代表正確的執行邏輯,有時候可能正好是巧合而已

 

二、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這個頭文件中,定義如下:

1 #ifndef NS_DESIGNATED_INITIALIZER
2 #if __has_attribute(objc_designated_initializer)
3 #define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
4 #else
5 #define NS_DESIGNATED_INITIALIZER
6 #endif
7 #endif

"__has_attribute"是Clang 的一個用於檢測當前編譯器是否支持某一特性的一個宏,對你沒有聽錯,"__has_attribute" 也是一個宏。詳細信息見: Type Safety Checking

通過上面的定義,我們可以看到"NS_DESIGNATED_INITIALIZER"其實是給初始化函數聲明的后面加上了一個編譯器可見的標記,不要小看這個標記,他可以在編譯時就幫我們找出一些潛在的問題,避免程序運行時出現一些奇奇怪怪的行為。

聽着神乎其神,編譯器怎么幫我們避免呢?

答案是:⚠️⚠️⚠️警告

如下圖:

 

編譯器出現警告,說明我們寫的代碼不夠規范。Xcode自帶的Analytics工具可以幫助我們找出程序的潛在的問題,多花點時間規范自己的代碼,消除項目中的警告,避免后面項目上線后出現奇奇怪怪的問題。

 

三、NS_DESIGNATED_INITIALIZER 正確使用姿勢是什么?

指定初始化函數 Vs 便利初始化函數

指定初始化函數對一個類來說非常重要,通常參數也是最多的,試想每次我們需要創建一個自定義類都需要一堆參數,那豈不是很痛苦。便利初始化函數就是用來幫我們解決這個問題的,可以讓我們比較的創建對象,同時又可以保證類的成員變量被設置為默認的值。

不過需要注意,為了享受這些“便利”,我們需要遵守一些規范,官方文檔鏈接如下:

Objective-C: Object InitializationMultiple initializers

Swift: Swift Initialization

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];

如果子類沒有重寫父類的所有初始化函數,而使用者恰好直接使用父類的初始化函數初始化對象,那么子類的成員變量就可能存在沒有正確初始化的情況。

 

四、舉個栗子

以上三條規范理解起來可能有點兒繞,我寫了個簡單的例子有助於理解該規范,代碼如下:

 1 @interface Animal : NSObject {
 2     NSString *_name;
 3 }
 4 
 5 - (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
 6 
 7 @end
 8 
 9 @implementation Animal
10 
11 - (instancetype)initWithName:(NSString *)name
12 {
13     self = [super init];
14     if (self) {
15         _name = name;
16     }
17 
18     return self;
19 }
20 
21 - (instancetype)init
22 {
23     return [self initWithName:@"Animal"];
24 }
25 
26 @end
27 
28 
29 @interface Mammal : Animal {
30     NSInteger   _numberOfLegs;
31 }
32 
33 - (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs NS_DESIGNATED_INITIALIZER;
34 
35 - (instancetype)initWithLegs:(NSInteger)numberOfLegs;
36 
37 @end
38 
39 @implementation Mammal
40 
41 - (instancetype)initWithLegs:(NSInteger)numberOfLegs
42 {
43     self = [self initWithName:@"Mammal"];
44     if (self) {
45         _numberOfLegs = numberOfLegs;
46     }
47 
48     return self;
49 }
50 
51 - (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs
52 {
53     self = [super initWithName:name];
54     if (self) {
55         _numberOfLegs = numberOfLegs;
56     }
57 
58     return self;
59 }
60 
61 - (instancetype)initWithName:(NSString *)name
62 {
63     return [self initWithName:name andLegs:4];
64 }
65 
66 @end
67 
68 
69 @interface Whale : Mammal {
70     BOOL    _canSwim;
71 }
72 
73 - (instancetype)initWhale NS_DESIGNATED_INITIALIZER;
74 
75 @end
76 
77 @implementation Whale
78 
79 - (instancetype)initWhale
80 {
81     self = [super initWithName:@"Whale" andLegs:0];
82     if (self) {
83         _canSwim = YES;
84     }
85 
86     return self;
87 }
88 
89 - (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs
90 {
91     return [self initWhale];
92 }
93 
94 - (NSString *)description
95 {
96     return [NSString stringWithFormat:@"Name: %@, Numberof Legs %zd, CanSwim %@", _name, _numberOfLegs, _canSwim ? @"YES" : @"NO"];
97 }
98 
99 @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;
}

細心的同學會發現,無論你使用父類的還是爺爺類的初始化函數創建子類的對象,最后四個調用順序都為:⑧ -> ⑤ -> ③ -> ①。

指定初始化函數規則只能用來保證對象的創建過程是從跟類到子類依次初始化所有成員變量,無法解決業務問題。

 

五、當 initWithCoder: 遇到 NS_DESIGNATED_INITIALIZER

NSCoding協議的定義如下:

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER

@end

蘋果官方文檔 Decoding an Object 中明確規定:

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協議的三條原則。

 

六、總結

上面關於指定初始化的規則講了那么多,其實可以歸納為兩點:

  • 便利初始化函數只能調用自己類中的其他初始化方法
  • 指定初始化函數才有資格調用父類的指定初始化函數

蘋果官方有個圖,有助於我們理解這兩點:

當我們為自己創建的類添加指定初始化函數時,必須准確的識別並覆蓋直接父類所有的指定初始化函數,這樣才能保證整個子類的初始化過程可以覆蓋到所有繼承鏈上的成員變量得到合適的初始化。

NS_DESIGNATED_INITIALIZER 是一個很有用的宏,充分發揮編譯器的特性幫我們找出初始化過程中可能存在的漏洞,增強代碼的健壯性。

 

參考資料:

Object creation

Initialization

Multiple initializers

How To: Objective C Initializer Patterns

Decoding an Object

Object Initialization

 

注:轉載請著名出處,有什么問題歡迎留言

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM