Objective-C 有兩個神奇的方法:+load 和 +initialize,這兩個方法在類被使用時會自動調用。但是兩個方法的不同點會導致應用層面上性能的顯著差異。
一、+ initialize 方法和+load 調用時機
先來看一個表
方法 | +(void)load | +(void)initialize |
---|---|---|
執行時機 | 在程序運行后立即執行 | 在類的方法第一次被調時執行 |
若自身未定義,是否沿用父類的方法? | 否 (這是由於+load方法是根據方法地址直接調用, 並不是經過objc_msgSend函數調用。下面有解釋) |
是 |
分類中的定義 | 全都執行,但后於類中的方法 覆蓋類中的方法, | 只執行一個 |
執行次數(非主動調用的情況下) | 必然一次 | 0、1、多 次(調用者會不同) |
先看官方解釋:
- 首先說一下 + initialize 方法:蘋果官方對這個方法有這樣的一段描述:這個方法會在 第一次初始化這個類之前 被調用,我們用它來初始化靜態變量。
- load 方法會在加載類的時候就被調用,也就是 ios 應用啟動的時候,就會加載所有的類,就會調用每個類的 + load 方法。
- 之后我們結合代碼來探究一下 + initialize 與 + load 兩個方法的調用時機,首先是 + load:
#pragram ---main函數中的代碼--- #import <UIKit/UIKit.h> #import "AppDelegate.h" int main(int argc, char * argv[]) { NSLog(@"%s",__func__); @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } #pragram ---基於NSObject的Person類--- #import "Person.h" @implementation Person + (void)load{ NSLog(@"%s",__func__); } + (void)initialize{ [super initialize]; NSLog(@"%s %@",__func__,[self class]); } - (instancetype)init{ if (self = [super init]) { NSLog(@"%s",__func__); } return self; } @end #pragram ---基於Person的Son類--- #import "Girl.h" @implementation Girl + (void)load{ NSLog(@"%s ",__func__); } + (void)initialize{ [super initialize]; NSLog(@"%s ",__func__); } - (instancetype)init{ if (self = [super init]) { NSLog(@"%s",__func__); } return self; } @end
運行程序,我們看一下輸出日志:
2015-10-27 15:21:07.545 initialize[11637:334237] +[Person load] 2015-10-27 15:21:07.546 initialize[11637:334237] +[Girl load] 2015-10-27 15:21:07.546 initialize[11637:334237] main
這說明在我並沒有對類做任何操作的情況下,+load 方法會被默認執行,並且是在 main 函數之前執行的。
二、接下來我們來查看一下 + initialize 方法,先在 ViewController 中創建 Person 和 Girl 對象:
#import "ViewController.h" #import "Person.h" #import "Son.h" #import "Girl.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person * a = [Person new]; Person * b = [Person new]; Girl *c = [Girl new]; Girl *d = [Girl new]; } @end
下面我們來看一下輸出日志:
2015-10-27 15:33:56.195 initialize[11711:342410] +[Person load] 2015-10-27 15:33:56.196 initialize[11711:342410] +[Girl load] 2015-10-27 15:33:56.197 initialize[11711:342410] main 2015-10-27 15:33:56.259 initialize[11711:342410] +[Person initialize] Person 2015-10-27 15:33:56.259 initialize[11711:342410] -[Person init] 2015-10-27 15:33:56.259 initialize[11711:342410] -[Person init] 2015-10-27 15:33:56.259 initialize[11711:342410] +[Girl initialize] 2015-10-27 15:33:56.260 initialize[11711:342410] -[Girl init] 2015-10-27 15:33:56.260 initialize[11711:342410] -[Girl init]
通過這個實驗我們可以確定兩點:
- + initialize 方法類似一個懶加載,如果沒有使用這個類,那么系統默認不會去調用這個方法,且默認只加載一次;
- + initialize 的調用發生在 +init 方法之前。
三、接下來再探究一下 + initialize 在父類與子類之間的關系,創建一個繼承自 Person 類的 Son類:
#pragram ---ViewController 中的代碼--- #import "ViewController.h" #import "Person.h" #import "Son.h" #import "Girl.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person * a = [Person new]; Person * b = [Person new]; Son*z = [Son new]; } @end
看一下輸出日志:
2015-10-27 15:44:55.762 initialize[12024:351576] +[Person load] 2015-10-27 15:44:55.764 initialize[12024:351576] +[Son load] 2015-10-27 15:44:55.764 initialize[12024:351576] +[Girl load] 2015-10-27 15:44:55.764 initialize[12024:351576] main 2015-10-27 15:44:55.825 initialize[12024:351576] +[Person initialize] Person 2015-10-27 15:44:55.825 initialize[12024:351576] -[Person init] 2015-10-27 15:44:55.825 initialize[12024:351576] -[Person init] 2015-10-27 15:44:55.826 initialize[12024:351576] +[Person initialize] Son 2015-10-27 15:44:55.826 initialize[12024:351576] -[Person init]
我們會發現 Person 類的 + initialize 方法又被調用了,但是查看一下是子類 Son 調用的,也就是創建子類的時候,子類會去調用父類的 + initialize 方法。
四、總結
- 如果你實現了 + load 方法,那么當類被加載時它會自動被調用。這個調用非常早。
如果你實現了一個應用或框架的 + load,並且你的應用鏈接到這個框架上了,那么 + load 會在 main() 函數之前被調用。
如果你在一個可加載的 bundle 中實現了 + load,那么它會在 bundle 加載的過程中被調用。比如方法交換等 - + initialize 方法的調用看起來會更合理,通常在它里面寫代碼比在 + load 里寫更好。
+ initialize 很有趣,因為它是懶調用的,也有可能完全不被調用。類第一次被加載時,
五、其他內部實現原理補充
一、+load 調用時機和順序原理解析
load 方法在什么時候調用?
官方解釋是:運行時,添加類或者分類的時候調用。實現此方法以在加載時執行特定於類的行為。
+load
方法是一定會在runtime中被調用的,只要類被添加到runtime中了,就會調用+load
方法,即只要是在Compile Sources
中出現的文件總是會被裝載,與這個類是否被用到無關,因此+load
方法總是在main函數之前調用。所以我們可以自己實現+laod
方法來在這個時候執行一些行為。
換句話說,load是在app啟動的時候加載並在main函數之前調用,從源碼中找
_objc_init —>
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
這里面的2個方法 map_images 和 load_images, map_images的作用就是加載所有的類/協議/分類,加載完成之后,就開始調用load_images,在這個方法里面看:
load_images(const char *path __unused, const struct mach_header *mh) { ...... { mutex_locker_t lock2(runtimeLock); prepare_load_methods((const headerType *)mh); // 把所有需要load的類 加載一個list里面 } call_load_methods(); // 調用load方法 } void call_load_methods(void) { ...... do { while (loadable_classes_used > 0) { call_class_loads(); // 先加載類的load } more_categories = call_category_loads(); // 在加載category的load } while (loadable_classes_used > 0 || more_categories); ...... }
另外有意思的一點是:
+load
方法不會覆蓋(因為由於+load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用)。
也就是說,如果子類實現了+load
方法,那么會先調用父類的+load
方法,然后又去執行子類的+load
方法。
但要注意的時+load方法只會調用一次。而且執行順序是:類 ->父類-> 子類 ->分類。而不同類之間的順序不一定。
但是這里依然有一個疑問,官方解釋沒有說清楚,1、分類的加載順序是怎樣的?
其實在源碼中有可以看到:
void prepare_load_methods(const headerType *mhdr) { ...... classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count); // 1、按照編譯順序加載所有的類(不包括分類) for (i = 0; i < count; i++) { schedule_class_load(remapClass(classlist[i])); // 2、在這里 按照先父類 在子類的方式加入列表 } category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); // 分類也是類似的方式 for (i = 0; i < count; i++) { ...... add_category_to_loadable_list(cat); } } static void schedule_class_load(Class cls) { ....... schedule_class_load(cls->superclass); //遞歸加載,先加載父類 add_class_to_loadable_list(cls); }
1、先加載類的load:類的加載是按照編譯順序,同時遵循先父類再子類的方式
2、再加載分類的load:分類直接按照編譯順序,和其綁定類的繼承沒有關系
總結
1、先加載類的load
2、再加載分類的load
3、不同的類之間加載load順序為:有繼承關系的,先加載父類load、再加載子類的load,無繼承關系的,按照編譯順序
----比如順序二 Student、OtherClass、Person,先加載Student的load,由於Person是Student的父類,所以Person的順序比OtherClass早
4、分類的加載順序是完全按照編譯順序,也就是誰在前面,誰先加載。和其綁定類的繼承關系無關
----比如順序二中,Student繼承Person,但是其分類的順序是 Student+JE2、Student+JE1、Person+JE,順序是什么樣,加載load就是什么樣。
5、即使有類的源文件,但是編譯列表中沒有,那么這個類就不會被編譯,也就不會執行其load方法
2、+initialize
initialize方法在什么時候調用?
官方解釋是:在類收到第一條消息之前初始化它。
換句話說,就是第一次用到它之前調用,比如初始化一個對象(其父類也會調用initialize)、調用類方法等。 從源碼中找:
- 說明:沒有發送消息的類不會調用initialize
- 如果主類有相應的分類(或多個分類),會調用分類中的initialize方法,具體調用的是哪個分類的方法,由編譯順序決定。
- 當子類沒有重寫initialize方法,這個時候回去執行父類的initialize方法
class_initialize -> initializeNonMetaClass() void initializeNonMetaClass(Class cls) { ....... // Make sure super is done initializing BEFORE beginning to initialize cls. // See note about deadlock above. supercls = cls->superclass; if (supercls && !supercls->isInitialized()) { initializeNonMetaClass(supercls); // 遞歸加載父類的initialize } ........ }
+initialize
方法是在類或它的子類收到第一條消息之前被調用的,這里所指的消息包括實例方法和類方法的調用,並且只會調用一次。+initialize
方法實際上是一種惰性調用,也就是說如果一個類一直沒被用到,那它的+initialize
方法也不會被調用,這一點有利於節約資源。
與+load
方法不同,卻更符合我們預期的就是,+initialize
方法會覆蓋是因為調用的objc_msgSend。
也就是說如果子類實現了+initialize
方法,就不會執行父類的了,直接執行子類本身的。
如果分類實現了+initialize
方法,也不會再執行主類的。
所以+initialize
方法的執行覆蓋順序是:分類 -> 子類 ->類。且只會有一個+initialize
方法被執行。
總結
1、initialize的執行順序為先父類、在子類
2、分類的initialize方法會覆蓋主類的方法(假覆蓋,方法都在,只是沒有執行)
3、只有在這個類有發送消息的時候才會執行initialize,比如初始化對象、調用類方法等。
4、多個分類的情況,只執行一次,具體執行哪個分類的initialize,有編譯順序決定(Build Phases -> Compile Sources 中的順序)
5、如果子類沒有重寫initialize,那么會調用其父類的initialize方法
3、兩者的異同
1、相同點
1).load和initialize會被自動調用,不能手動調用它們。
2).子類實現了load和initialize的話,會隱式調用父類的load和initialize方法。
3).load和initialize方法內部使用了鎖,因此它們是線程安全的。
2、不同點
1).調用順序不同,
- 以main函數為分界,
+load
方法在main函數之前執行, +initialize
在main函數之后執行。
2).若自身未定義,是否沿用父類的方法:
- 子類中沒有實現
+load
方法的話,不會調用父類的+load
方法;(不會) - 而子類如果沒有實現
+initialize
方法的話,也會自動調用父類的+initialize
方法。(會)
3).調用時機:
+load
方法是在類被裝載進來的時候就會調用,+initialize
在第一次給某個類發送消息時調用(比如實例化一個對象),並且只會調用一次,是懶加載模式,如果這個類一直沒有使用,就不回調用到+initialize
方法。
4).調用方式:
- load是根據函數地址直接調用
- initialize是通過objc_msgSend調用
4、使用場景
1.+load
一般是用來交換方法Method Swizzle
,由於它是線程安全的,而且一定會調用且只會調用一次,通常在使用UrlRouter的時候注冊類的時候也在+load
方法中注冊。
2.+initialize
方法主要用來對一些不方便在編譯期初始化的對象進行賦值,或者說對一些靜態常量進行初始化操作。
5、為什么原本類的+load方法不會被分類的+load方法覆蓋呢?
這是由於+load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用。
+load方法相關源碼閱讀順序:
- _objc_init
- load_images
- prepare_load_methods
- schedule_class_load
- add_class_to_loadable_list
- add_category_to_loadable_list
- calls
- call_class_loads
- call_category_loads
- (*load_method)(cls, SEL_load)
3、+initialize方法調用的時機
+initialize方法會在類第一次接收到消息時調用。
調用順序:先調用父類的+initialize,再調用子類的+initialize,每個類只會初始化1次
+initialize方法相關源碼閱讀順序:
-
objc-msg-arm64.s
- objc_msgSend
-
objc-runtime-new.mm
- class_getInstanceMethod
- lookUpImpOrNil
- lookUpImpOrForward
- _class_initialize
- callInitialize
- objc_msgSend(cls, SEL_initialize)
+initialize和+load的最大區別是,+initialize是通過objc_msgSend進行調用的,所以有以下特點:
- 如果子類沒有實現+initialize,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)
- 如果分類實現了+initialize,就覆蓋類本身的+initialize調用