Objc類的載入和初始化(+load和+initialize方法)
作為一個程式設計師,絕大多數時候你都不需要關心一個類是怎麼被載入進記憶體的。這裡面 runtime linker 在你的程式碼還沒跑起來之前就已經做了很多複雜的工作。
對於大多類來說,知道這一點就已經相當足夠了。但是,有一些類可能需要做一些特殊的準備工作。比如初始化一個全域性的表,從 UserDefaults 裡面讀取配置並快取起來,又或者做一些其他的準備工作。
ObjC 提供了兩種方法來實現這些事情:
1 2 |
+ initialize
+ load
|
+load
如果你的類實現了 +load 方法,這個方法就會在類被載入的時候呼叫。這個呼叫時機是很早的。如果你是在被其他應用引用的應用(Application)或框架(Framework)裡實現了這個方法,它甚至會比 main() 函式還早被觸發。如果你是在一個可以被載入的 bundle 裡面實現這個方法,那當 bundle 被載入的時候這個方法就會被呼叫。
因為 +load 方法過早被呼叫,所以應用起來會有點困難。很多時候有些類是要比別人更早被載入的,這樣你無法判斷別人是不是早就被呼叫過 +load 方法了。更糟糕的時候,你的應用中包含的 C++ 靜態初始化函式在這個時機點是還沒被呼叫的,如果你在 +load 裡面呼叫了相關的程式碼,就很有可能會 crash。好訊息是你連結的 frameworks 是保證在 +load 呼叫前就載入過了的,所以在這裡使用 framewroks 是安全的。還有父類也是保證完全載入過了,所以使用父類也是沒問題的。
+load 這個方法有一個有意思的特性,就是 runtime 會把所有 category 裡面實現了 +load 的方法全部調一遍。也就是說如果你在多個 category 裡面都實現了 +load 方法,這些方法都會被呼叫一次。這種設計可能跟你認識到 category 的機制完全相反,不過你要知道 +load 方法不是一個普通的方法。這個特性決定了 +load 是一個幹壞事的絕佳場所,比如 swizzling。
+initialize
相比而言,+initialize 方法就要正常的多了,通常也是一個更好的安置程式碼的地方。+initialize 有意思的地方在於它會很晚才被呼叫,甚至它有可能完全不會被呼叫。當一個類被載入的時候,+initialize 不會被呼叫,當一個訊息傳送給這個類的時候(譯者注:ObjC 的方法呼叫都是通過 runtime 的訊息機制,objc_sendMsg 方法),runtime 就會檢查這個方法有沒有被呼叫過,如果沒有就呼叫之。大概可以認為是這樣的:
1 2 3 4 5 6 |
id objc_msgSend(id self, SEL _cmd, …)
{
if (!self->class->initialized)
[self->class initialize];
…send the message…
}
|
當然真正的實現不會這麼簡單,還要解決執行緒安全之類的問題,不過大概就是這麼個意思吧。每個類知會呼叫一次 +initialize 方法,而且只會在這個類收到第一個訊息的時候被呼叫。跟 +load 方法一樣,+initialize 會先呼叫這個類所有的父類,最後才調到自己的 +initialize 方法。
這就使得 +initialize 用起來要比 +load 方法更安全,因為呼叫時機的環境要安全得多。當然這時候的環境還要取決於第一條訊息傳送的結果,不過可以保證呼叫的時機一定比 NSApplicationMain() 要晚。
由於 +initialize 是 lazily run 的,所以這裡就不是做註冊事件的好地方。比如說,NSValueTransformer 和 NSURLProtocol 就不能用 +initialize 來註冊自己,因為這就成了一個先有雞還是先有蛋的問題。
這個方法適合用來做需要在類被載入後做的事情。由於這個方法執行的時候環境容錯性更好,所以你可以使用的方法也就比 +load 自由得多,也因為這個方法是 lazy 呼叫的,所以你在這個方法中使用的資源就不會事先申請而造成浪費。
+initialize 的使用還有個小伎倆,我上面的虛擬碼裡提到 runtime 會呼叫:
1 |
[self->class initialize]
|
這就導致 ObjC 做 selector 實現的檢查,如果當前類沒有實現這個方法,那麼父類的方法就會被呼叫。不只在虛擬碼裡,實際上也是這樣的。所以,你的 +initialize 就得寫成下面這樣:
1 2 3 4 5 6 7 |
+ (void)initialize
{
if (self == [WhateverClass class])
{
…perform initialization…
}
}
|
如果沒有做這個檢查,如果你有沒實現 +initialize 的子類,你的程式碼就會被呼叫兩次。就算你沒有任何子類,Apple 的 KVO 也會動態建立沒有實現 +initialize 的子類。
結論
ObjC 提供了兩種自動執行類初始化程式碼的方法。+load 方法保證了會在 class 被載入的時候呼叫,這個時機很早,所以對於需要很早被執行的程式碼來說是很有用的。但是在這個時機跑的程式碼也可以是很危險的,畢竟這個時候的環境比較惡劣。
由於 +initialize 方法是 lazy 觸發的,所以對於初始化設定的環境就要友好得多。只要不是在類接收第一條訊息之前一定要做的事情,都可以在這個方法裡面做。