## Initializer 和 dealloc 初始化
推薦的代碼組織方式:將 `dealloc` 方法放在實現文件的最前面(直接在 `@synthesize` 以及 `@dynamic` 之后),`init` 應該放在 `dealloc` 之后。如果有多個初始化方法, designated initializer 應該放在第一個,secondary initializer 在之后緊隨,這樣邏輯性更好。如今有了 ARC,dealloc 方法幾乎不需要實現,不過把 init 和 dealloc 放在一起可以從視覺上強調它們是一對的。通常,在 init 方法中做的事情需要在 dealloc 方法中撤銷。
`init` 方法應該是這樣的結構:
~~~
- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
~~~
為什么設置 `self` 為 `[super init]` 的返回值,以及中間發生了什么呢?這是一個十分有趣的話題。
讓我們后退一步:我們曾經寫了類似 `[[NSObject alloc] init]` 的表達式, `alloc` 和 `init` 區別慢慢褪去 。一個 Objective-C 的特性叫 _兩步創建_ 。 這意味著申請分配內存和初始化是兩個分離的操作。
- `alloc`表示對象分配內存,這個過程涉及分配足夠的可用內存來保存對象,寫入`isa`指針,初始化 retain 的計數,并且初始化所有實例變量。
- `init` 是表示初始化對象,這意味著把對象放到了一個可用的狀態。這通常是指把對象的實例變量賦給了可用的值。
`alloc` 方法會返回一個合法的沒有初始化的實例對象。每一個發送到實例的信息會被翻譯為名字是 `self` 的 `alloc` 返回的指針的參數返回的 `objc_msgSend()` 的調用。這樣之后 `self` 已經可以執行所有方法了。
為了完成兩步創建,第一個發送給新創建的實例的方法應該是約定俗成的 `init` 方法。注意 `NSObject` 的 `init` 實現中,僅僅是返回了 `self`。
關于 `init` 有一個另外的重要的約定:這個方法可以(并且應該)在不能成功完成初始化的時候返回 `nil`;初始化可能因為各種原因失敗,比如一個輸入的格式錯誤,或者未能成功初始化一個需要的對象。這樣我們就理解了為什么需要總是調用 `self = [super init]`。如果你的超類沒有成功初始化它自己,你必須假設你在一個矛盾的狀態,并且在你的實現中不要處理你自己的初始化邏輯,同時返回 `nil`。如果你不是這樣做,你看你會得到一個不能用的對象,并且它的行為是不可預測的,最終可能會導致你的 app 發生 crash。
重新給 `self` 賦值同樣可以被 `init` 利用為在被調用的時候返回不同的實例。一個例子是 [類簇](#) 或者其他的返回相同的(不可變的)實例對象的 Cocoa 類。
## Designated 和 Secondary Initializers
Objective-C 有 designated 和 secondary 初始化方法的觀念。designated 初始化方法是提供所有的參數,secondary 初始化方法是一個或多個,并且提供一個或者更多的默認參數來調用 designated 初始化方法的初始化方法。
~~~
@implementation ZOCEvent
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
location:(CLLocation *)location
{
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
{
return [self initWithTitle:title date:date location:nil];
}
- (instancetype)initWithTitle:(NSString *)title
{
return [self initWithTitle:title date:[NSDate date] location:nil];
}
@end
~~~
`initWithTitle:date:location:` 就是 designated 初始化方法,另外的兩個是 secondary 初始化方法。因為它們僅僅是調用類實現的 designated 初始化方法
### Designated Initializer
一個類應該又且只有一個 designated 初始化方法,其他的初始化方法應該調用這個 designated 的初始化方法(雖然這個情況有一個例外)
這個分歧沒有要求那個初始化函數需要被調用。
在類繼承中調用任何 designated 初始化方法都是合法的,而且應該保證 _所有的_ designated initializer 在類繼承中是是從祖先(通常是 `NSObject`)到你的類向下調用的。
實際上這意味著第一個執行的初始化代碼是最遠的祖先,然后從頂向下的類繼承,所有類都有機會執行他們特定的初始化代碼。這樣,你在你做你的特定的初始化工作前,所有你從超類繼承的東西是不可用的狀態。即使它的狀態不明確,所有 Apple 的框架的 Framework 是保證遵守這個約定的,而且你的類也應該這樣做。
當定義一個新類的時候有三個不同的方式:
1. 不需要重載任何初始化函數
1. 重載 designated initializer
1. 定義一個新的 designated initializer
第一個方案是最簡單的:你不需要增加類的任何初始化邏輯,只需要依照父類的designated initializer。當你希望提供額外的初始化邏輯的時候你可以重載 designated initializer。你只需要重載你的直接的超類的 designated initializer 并且確認你的實現調用了超類的方法。你一個典型的例子是你創造`UIViewController`子類的時候重載`initWithNibName:bundle:`方法。
~~~
@implementation ZOCViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call to the superclass designated initializer
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
@end
~~~
在 `UIViewController` 子類的例子里面如果重載 `init` 會是一個錯誤,這個情況下調用者會嘗試調用 `initWithNib:bundle` 初始化你的類,你的類實現不會被調用。著同樣違背了它應該是合法調用任何 designated initializer 的規則。
In case you want to provide your own designated initializer there are three steps that you need to follow in order to guarantee the correct behavior:
在你希望提供你自己的初始化函數的時候,你應該遵守這三個步驟來保證正確的性:
1. 定義你的 designated initializer,確保調用了直接超類的 designated initializer
1. 重載直接超類的 designated initializer。調用你的新的 designated initializer.
1. 為新的 designated initializer 寫文檔
很多開發者忽略了后兩步,這不僅僅是一個粗心的問題,而且這樣違反了框架的規則,而且可能導致不確定的行為和bug。讓我們看看正確的實現的例子:
~~~
@implementation ZOCNewsViewController
- (id)initWithNews:(ZOCNews *)news
{
// call to the immediate superclass's designated initializer
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// Override the immediate superclass's designated initializer (重載直接父類的 designated initializer)
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call the new designated initializer
return [self initWithNews:nil];
}
@end
~~~
你沒重載 `initWithNibName:bundle:` 而且調用者決定用這個方法初始化你的類(這是完全合法的)。 `initWithNews:` 永遠不會被調用,所以導致了不正確的初始化流程,你的類特定的初始化邏輯沒有被執行。
即使可以推斷那個方法是 designate initializer它,但是最好清晰地明確(未來的你或者其他開發者在改代碼的時候會感謝你的)。你應該考慮來用這兩個策略(不是互斥的):第一個是你在文檔中明確哪一個初始化方法是 designated 的,但是最好你可以用編譯器的指令 `__attribute__((objc_designated_initializer))` 來標記你的意圖。
用這個編譯指令的時候,編譯器回來幫你。如果你的新的 designate initializer 沒有調用你超類的 designated initializer,上編譯器會發出警告。然而,當沒有調用類的 designated initializer 的時候(并且依次提供必要的參數),并且調用其他父類中的 designated initialize 的時候,會變成一個不可用的狀態。參考之前的例子,當實例化一個 `ZOCNewsViewController` 展示一個新聞而那條新聞沒有展示的話,就會毫無意義。這個情況下你應該只需要讓其他的 designated initializer 失效,來強制調用一個非常特別的 designated initializer。通過使用另外一個編譯器指令 `__attribute__((unavailable("Invoke the designated initializer")))` 來修飾一個方法,通過這個屬性,會讓你在試圖調用這個方法的時候產生一個編譯錯誤。
這是之前的例子相關的實現的頭文件(這里使用宏來讓代碼沒有那么啰嗦)
~~~
@interface ZOCNewsViewController : UIViewController
- (instancetype)initWithNews:(ZOCNews *)news ZOC_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
- (instancetype)init ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
@end
~~~
上述的一個推論是:你應該永遠不從 designated initializer 里面調用一個 secondary initializer (如果secondary initializer 遵守約定,它會調用 designated initializer)。如果這樣,調用很可能會調用一個子類重寫的 init 方法并且陷入無限遞歸之中。
然而一個意外是一個對象是否遵守 `NSCoding` 協議,并且它通過方法 `initWithCoder:` 初始化。我們應該區別超類是否符合 `NSCoding` 的情況。
如果符合,如果你只是調用 `[super initWithCoder:]` 你會可能有一個共享的初始化代碼在 designated initializer 里面,一個好的方法是吧這些代碼放在私有方法里面(比如 `p_commonInit` )。
當你的超類不符合`NSCoding` 協議的時候,推薦把 `initWithCoder:` 作為 secondary initializer 來對待,并且調用 `self` 的 designated initializer。 注意這是違反 Apple 的 [Archives and Serializations Programming Guide](https://developer.apple.com/library/mac/documentation/cocoa/Conceptual/Archiving/Articles/codingobjects.html#//apple_ref/doc/uid/20000948-BCIHBJDE) 上面寫的:
> the object should first invoke its superclass's designated initializer to initialize inherited state (對象總是應該首先調用超類的 designated initializer 來初始化繼承的狀態)
如果你的類不是 `NSObject` 的直接子類,這樣做的話,會導致不可預測的行為。
### Secondary Initializer
正如之前的描述么,secondary initializer 是一種方便提供默認值、行為到 designated initializer 的 方法。也就是說,你不應該強制很多初始化操作在這樣的方法里面,并且你應該一直假設這個方法不會得到調用。我們保證的是唯一被調用的方法是 designated initializer。
這意味著你的 designated initializer 總是應該調用其他的 secondary initializer 或者你 `self` 的 designated initializer。有時候,因為錯誤,可能打成了 `super`,這樣會導致不符合上面提及的初始化順序(在這個特別的例子里面,是跳過當前類的初始化)
### References 參考
- [https://developer.apple.com/library/ios/Documentation/General/Conceptual/DevPedia-CocoaCore/ObjectCreation.html](https://developer.apple.com/library/ios/Documentation/General/Conceptual/DevPedia-CocoaCore/ObjectCreation.html)
- [https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/Initialization/Initialization.html](https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/Initialization/Initialization.html)
- [https://developer.apple.com/library/ios/Documentation/General/Conceptual/DevPedia-CocoaCore/MultipleInitializers.html](https://developer.apple.com/library/ios/Documentation/General/Conceptual/DevPedia-CocoaCore/MultipleInitializers.html)
- [https://blog.twitter.com/2014/how-to-objective-c-initializer-patterns](https://blog.twitter.com/2014/how-to-objective-c-initializer-patterns)
## instancetype
我們經常忽略 Cocoa 充滿了約定,并且這些約定可以幫助編譯器變得更加聰明。無論編譯器是否遭遇 `alloc` 或者 `init` 方法,他會知道,即使返回類型都是 `id` ,這些方法總是返回接受到的類類型的實例。因此,它允許編譯器進行類型檢查。(比如,檢查方法返回的類型是否合法)。Clang的這個好處來自于 [related result type](http://clang.llvm.org/docs/LanguageExtensions.html#related-result-types), 意味著:
> messages sent to one of alloc and init methods will have the same static type as the instance of the receiver class (發送到 alloc 或者 init 方法的消息會有同樣的靜態類型檢查是否為接受類的實例。)
更多的關于這個自動定義相關返回類型的約定請查看 Clang Language Extensions guide 的[appropriate section](#))
一個相關的返回類型可以明確地規定用 `instancetype` 關鍵字作為返回類型,并且它可以在一些工廠方法或者構造器方法的場景下很有用。它可以提示編譯器正確地檢查類型,并且更加重要的是,這同時適用于它的子類。
~~~
@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name;
@end
~~~
雖然如此,根據 clang 的定義,`id` 可以被編譯器提升到 `instancetype` 。在 `alloc` 或者 `init` 中,我們強烈建議對所有返回類的實例的類方法和實例方法使用 `instancetype` 類型。
在你的 API 中要構成習慣以及保持始終如一的,此外,通過對你代碼的小調整你可以提高可讀性:在簡單的瀏覽的時候你可以區分哪些方法是返回你類的實例的。你以后會感謝這些注意過的小細節的。
### 參考
- [http://tewha.net/2013/02/why-you-should-use-instancetype-instead-of-id/](http://tewha.net/2013/02/why-you-should-use-instancetype-instead-of-id/)
- [http://tewha.net/2013/01/when-is-id-promoted-to-instancetype/](http://tewha.net/2013/01/when-is-id-promoted-to-instancetype/)
- [http://clang.llvm.org/docs/LanguageExtensions.html#related-result-types](http://clang.llvm.org/docs/LanguageExtensions.html#related-result-types)
- [http://nshipster.com/instancetype/](http://nshipster.com/instancetype/)
## 初始化模式
### 類簇 (class cluster)
類簇在Apple的文檔中這樣描述:
> an architecture that groups a number of private, concrete subclasses under a public, abstract superclass. (一個在共有的抽象超類下設置一組私有子類的架構)
如果這個描述聽起來很熟悉,說明你的直覺是對的。 Class cluster 是 Apple 對[抽象工廠](http://en.wikipedia.org/wiki/Abstract_factory_pattern)設計模式的稱呼。
class cluster 的想法很簡單,你經常有一個抽象類在初始化期間處理信息,經常作為一個構造器里面的參數或者環境中讀取,來完成特定的邏輯并且實例化子類。這個"public facing" 應該知曉它的子類而且返回適合的私有子類。
這個模式非常有用,因為它減少了構造器調用中的復雜性,只需要知道接口如何與對象通信,而不需要知道怎么實現。
Class clusters 在 Apple 的Framework 中廣泛使用:一些明顯的例子比如 `NSNumber` 可以返回不同類型給你的子類,取決于 數字類型如何提供 (Integer, Float, etc...) 或者 `NSArray` 返回不同的最優存儲策略的子類。
這個模式的精妙的地方在于,調用者可以完全不管子類,事實上,這可以用在設計一個庫,可以用來交換實際的返回的類,而不用去管相關的細節,因為它們都遵從抽象超類的方法。
我們的經驗是使用類簇可以幫助移除很多條件語句。
一個經典的例子是如果你有為 iPad 和 iPhone 寫的一樣的 UIViewController 子類,但是在不同的設備上有不同的行為。
比較基礎的實現是用條件語句檢查設備,然后執行不同的邏輯。雖然剛開始可能不錯,但是隨著代碼的增長,運行邏輯也會趨于復雜。一個更好的實現的設計是創建一個抽象而且寬泛的 view controller 來包含所有的共享邏輯,并且對于不同設備有兩個特別的子例。
通用的 view controller 會檢查當前設備并且返回適當的子類。
~~~
@implementation ZOCKintsugiPhotoViewController
- (id)initWithPhotos:(NSArray *)photos
{
if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
self = nil;
if ([UIDevice isPad]) {
self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
}
else {
self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
}
return self;
}
return [super initWithNibName:nil bundle:nil];
}
@end
~~~
之前的代碼的例子展示了如何創建一個類簇。首先,`[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]` 來避免在子類中重載初始化方法,來避免無限的遞歸。當 `[[ZOCKintsugiPhotoViewController alloc] initWithPhotos:photos]` 得到調用的時候之前的檢查會變成 true 的,`self = nil` 是用來移除所有到 `ZOCKintsugiPhotoViewController` 實例的引用的,它會被釋放,按照這個邏輯來檢查哪個類應該被初始化。
讓我們假設在 iPhone 上運行了這個代碼, `ZOCKintsugiPhotoViewController_iPhone` 沒有重載`initWithPhotos:`,在這個情況下,當執行 `self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];` 的時候,`ZOCKintsugiPhotoViewController` 會被調用,并且當第一次檢查的時候,這樣不會讓 `ZOCKintsugiPhotoViewController` 檢查會變成 false 調用`return [super initWithNibName:nil bundle:nil];` ,這會讓 繼續初始化執行正確的初始化之前的會話。
### 單例
如果可能,請盡量避免使用單例而是依賴注入。然而,如果一定要用,請使用一個線程安全的模式來創建共享的實例。 對于GCD,用 `dispatch_once()` 函數就可以咯。
~~~
+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
~~~
使用dispatch_once(),來控制代碼同步,取代了原來老的約定俗成的用法。
~~~
+ (instancetype)sharedInstance
{
static id sharedInstance;
@synchronized(self) {
if (sharedInstance == nil) {
sharedInstance = [[MyClass alloc] init];
}
}
return sharedInstance;
}
~~~
`dispatch_once()` 的優點是,它更快,而且語法上更干凈,因為dispatch_once()的意思就是 ”把一些東西執行一次“,就像我們做的一樣。 這樣同時可以避免[possible and sometimes prolific crashes](http://cocoasamurai.blogspot.com/2011/04/singletons-your-doing-them-wrong.html).
經典的可以接受的單例對象的例子是一個設備的 GPS 以及 動作傳感器。即使單例對象可以被子類化,這個情況可以十分有用。這個接口應該證明給出的類是趨向于使用單例的。然而,經常使用一個單獨的公開的 `sharedInstance` 類方法就夠了,并且不可寫的屬性也應該被暴露。
把單例作為一個對象的容器來在代碼或者應用層面上共享是糟糕和丑陋的,這是一個不好的設計。