OC中有兩個特殊的類方法,分別是load
和initialize
。本文總結一下這兩個方法的區別於聯絡、使用場景和注意事項。Demo可以在我的Github上找到——load和initialize,如果覺得有幫助還望點個star以示支援,總結在文章末尾。
load
顧名思義,load
方法在這個檔案被程式裝載時呼叫。只要是在Compile Sources中出現的檔案總是會被裝載,這與這個類是否被用到無關,因此load
方法總是在main
函式之前呼叫。
呼叫規則
如果一個類實現了load
方法,在呼叫這個方法前會首先呼叫父類的load
方法。而且這個過程是自動完成的,並不需要我們手動實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// In Parent.m + (void)load { NSLog(@"Load Class Parent"); } // In Child.m,繼承自Parent + (void)load { NSLog(@"Load Class Child"); } // In Child+load.m,Child類的分類 + (void)load { NSLog(@"Load Class Child+load"); } // 執行結果: /* 2016-02-01 21:28:14.379 load[11789:1435378] Load Class Parent 2016-02-01 21:28:14.380 load[11789:1435378] Load Class Child 2016-02-01 22:28:14.381 load[11789:1435378] Load Class Child+load */ |
如果一個類沒有實現load
方法,那麼就不會呼叫它父類的load
方法,這一點與正常的類繼承和方法呼叫不一樣,需要額外注意一下。
執行順序
load
方法呼叫時,系統處於脆弱狀態,如果呼叫別的類的方法,且該方法依賴於那個類的load
方法進行初始化設定,那麼必須確保那個類的load
方法已經呼叫了,比如demo中的這段程式碼,列印出的字串就為null
:
1 2 3 4 5 6 7 8 9 10 |
// In Child.m + (void)load { NSLog(@"Load Class Child"); Other *other = [Other new]; [other originalFunc]; // 如果不先呼叫other的load,下面這行程式碼就無效,列印出null [Other printName]; } |
load
方法的呼叫順序其實有跡可循,我們看到demo的專案設定如下:
![](https://i.iter01.com/images/fd524212167d39ecb37d37eb9fab4e14bf234de1fcb3f67e021c93cddf442f0b.jpg)
在Compile Sources中,檔案的排放順序就是其轉載順序,自然也就是load
方法呼叫的順序。這一點也證明了load
方法中會自動呼叫父類的方法,因為在demo的輸出結果中,Parent
的load
方法先於Child
呼叫,而它的裝載順序其實在Child
之後。
雖然在這種簡單情況下我們可以辨別出各個類的load
方法呼叫的順序,但永遠不要依賴這個順序完成你的程式碼邏輯。一方面,這在後期的開發中極容易導致錯誤,另一方面,你實際上並不需要這麼做。
使用場景
由於呼叫load
方法時的環境很不安全,我們應該儘量減少load
方法的邏輯。另一個原因是load
方法是執行緒安全的,它內部使用了鎖,所以我們應該避免執行緒阻塞在load
方法中。
一個常見的使用場景是在load
方法中實現Method Swizzle:
1 2 3 4 5 6 7 |
// In Other.m + (void)load { Method originalFunc = class_getInstanceMethod([self class], @selector(originalFunc)); Method swizzledFunc = class_getInstanceMethod([self class], @selector(swizzledFunc)); method_exchangeImplementations(originalFunc, swizzledFunc); } |
在Child
類的load
方法中,由於還沒呼叫Other
的load
方法,所以輸出結果是”Original Output”,而在main函式中,輸出結果自然就變成了”Swizzled Output”。
一般來說,除了Method Swizzle,別的邏輯都不應該放在load
方法中實現。
initialize
這個方法在第一次給某個類傳送訊息時呼叫(比如例項化一個物件),並且只會呼叫一次。initialize
方法實際上是一種惰性呼叫,也就是說如果一個類一直沒被用到,那它的initialize
方法也不會被呼叫,這一點有利用節約資源。
呼叫規則
與load
方法類似的是,在initialize
方法內部也會呼叫父類的方法,而且不需要我們顯示的寫出來。與load
方法不同之處在於,即使子類沒有實現initialize
方法,也會呼叫父類的方法,這會導致一個很嚴重的問題:
1 2 3 4 5 6 7 8 9 10 |
// In Parent.m + (void)initialize { NSLog(@"Initialize Parent, caller Class %@", [self class]); } // In Child.m // 註釋掉initialize方法 // In main.m Child *child = [Child new]; |
執行後發現父類的initialize
方法竟然呼叫了兩次:
1 2 |
2016-02-01 22:57:02.985 load[12772:1509345] Initialize Parent, caller Class Parent 2016-02-01 22:57:02.985 load[12772:1509345] Initialize Parent, caller Class Child |
這是因為在建立子類物件時,首先要建立父類物件,所以會呼叫一次父類的initialize
方法,然後建立子類時,儘管自己沒有實現initialize
方法,但還是會呼叫到父類的方法。
雖然initialize
方法對一個類而言只會呼叫一次,但這裡由於出現了兩個類,所以呼叫兩次符合規則,但不符合我們的需求。正確使用initialize
方法的姿勢如下:
1 2 3 4 5 6 |
// In Parent.m + (void)initialize { if (self == [Parent class]) { NSLog(@"Initialize Parent, caller Class %@", [self class]); } } |
加上判斷後,就不會因為子類而呼叫到自己的initialize
方法了。
使用場景
initialize
方法主要用來對一些不方便在編譯期初始化的物件進行賦值。比如NSMutableArray
這種型別的例項化依賴於runtime的訊息傳送,所以顯然無法在編譯器初始化:
1 2 3 4 5 6 7 8 9 10 |
// In Parent.m static int someNumber = 0; // int型別可以在編譯期賦值 static NSMutableArray *someObjects; + (void)initialize { if (self == [Parent class]) { // 不方便編譯期複製的物件在這裡賦值 someObjects = [[NSMutableArray alloc] init]; } } |
總結
load
和initialize
方法都會在例項化物件之前呼叫,以main函式為分水嶺,前者在main函式之前呼叫,後者在之後呼叫。這兩個方法會被自動呼叫,不能手動呼叫它們。load
和initialize
方法都不用顯示的呼叫父類的方法而是自動呼叫,即使子類沒有initialize
方法也會呼叫父類的方法,而load
方法則不會呼叫父類。load
方法通常用來進行Method Swizzle,initialize
方法一般用於初始化全域性變數或靜態變數。load
和initialize
方法內部使用了鎖,因此它們是執行緒安全的。實現時要儘可能保持簡單,避免阻塞執行緒,不要再使用鎖。