本文譯自:https://www.mikeash.com/pyblog/friday-qa-2009-05-22-objective-c-class-loading-and-initialization.html
作為一個程序員,絕大多數時候你都不需要關心一個類是怎麼被加載進內存的。這裡面 runtime linker 在你的代碼還沒跑起來之前就已經做了很多復雜的工作。
對於大多類來說,知道這一點就已經相當足夠了。但是,有一些類可能需要做一些特殊的准備工作。比如初始化一個全局的表,從 UserDefaults 裡面讀取配置並緩存起來,又或者做一些其他的准備工作。
ObjC 提供了兩種方法來實現這些事情:
+ 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 就會檢查這個方法有沒有被調用過,如果沒有就調用之。大概可以認為是這樣的:
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 會調用:
[self->class initialize]
這就導致 ObjC 做 selector 實現的檢查,如果當前類沒有實現這個方法,那麼父類的方法就會被調用。不只在偽代碼裡,實際上也是這樣的。所以,你的 +initialize 就得寫成下面這樣:
+ (void)initialize { if(self == [WhateverClass class]) { …perform initialization… } }
如果沒有做這個檢查,如果你有沒實現 +initialize 的子類,你的代碼就會被調用兩次。就算你沒有任何子類,Apple 的 KVO 也會動態創建沒有實現 +initialize 的子類。
結論
ObjC 提供了兩種自動運行類初始化代碼的方法。+load 方法保證了會在 class 被加載的時候調用,這個時機很早,所以對於需要很早被執行的代碼來說是很有用的。但是在這個時機跑的代碼也可以是很危險的,畢竟這個時候的環境比較惡劣。
由於 +initialize 方法是 lazy 觸發的,所以對於初始化設置的環境就要友好得多。只要不是在類接收第一條消息之前一定要做的事情,都可以在這個方法裡面做。