之前在寫《Category你真的懂嗎?》那篇簡(jiǎn)書收集資料的時(shí)候,看了很多l(xiāng)oad和initialize的資料,加深了了解,打算寫一篇簡(jiǎn)書記錄一下。
load函數(shù)
1.load函數(shù)的加載時(shí)機(jī)
我們來(lái)看一下蘋果官方文檔的描述:
Invoked whenever a class or category is added to the Objective-C runtime.
當(dāng)class或者category添加到runtime的時(shí)候會(huì)被喚醒。對(duì)于動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)中的class和category都有效。程序代碼加載的順序?yàn)椋?/p>
1.調(diào)用所有Framework中的初始化函數(shù)
2.調(diào)用所有+load函數(shù)
3.調(diào)用C++靜態(tài)初始化函數(shù)和C/C++ __attribute__(constructor)函數(shù)
4.調(diào)用所有鏈接到目標(biāo)文件的Framework中的初始化函數(shù)
換句話來(lái)時(shí),load方法是在這個(gè)文件被程序裝載時(shí)調(diào)用,因此load方法是在main方法之前調(diào)用,看代碼:
@implementation ClassA
+ (void)load{
NSLog(@"load_class_a");
}
@end
@implementation ClassA (Addition)
+ (void)load {
NSLog(@"load_class_a_addition");
}
@end
2017-04-07 10:17:26.298950+0800 BasicTest[27006:1840166] load_class_a
2017-04-07 10:17:26.299157+0800 BasicTest[27006:1840166] load_class_a_addition
2017-04-07 10:17:26.299199+0800 BasicTest[27006:1840166] main
2.load函數(shù)的調(diào)用順序
看一下官方文檔描述:
A class’s +load method is called after all of its superclasses’ +load methods.
A category +load method is called after the class’s own +load method.
也就是load函數(shù)的加載順序?yàn)椋?strong>superClass -> class -> category,我們用代碼驗(yàn)證一下:
定義了一下類:
ClassA
ClassASub //ClassASub -> ClassA
ClassASub2 //ClassASub2 -> ClassA
ClassASubSub //ClassASubSub -> ClassASub -> ClassA
ClassACategory //ClassA(Category)
ClassASubCategory //ClassASub(Category)
//輸出
2017-04-07 14:07:33.481562+0800 BasicTest[32286:2163063] load_class_a
2017-04-07 14:07:33.481885+0800 BasicTest[32286:2163063] load_class_a_sub_2
2017-04-07 14:07:33.481907+0800 BasicTest[32286:2163063] load_class_a_sub
2017-04-07 14:07:33.481918+0800 BasicTest[32286:2163063] load_class_a_sub_sub
2017-04-07 14:07:33.481929+0800 BasicTest[32286:2163063] load_class_a_category
2017-04-07 14:07:33.481954+0800 BasicTest[32286:2163063] load_class_a_sub_category
2017-04-07 14:07:33.481999+0800 BasicTest[32286:2163063] main
很明顯系統(tǒng)會(huì)先調(diào)用所有類的+load()方法,然后再根據(jù)類調(diào)用相應(yīng)的category,并且也是父類的+load()方法先被調(diào)用。
3.load函數(shù)的作用和使用場(chǎng)景
由于load的調(diào)用時(shí)機(jī)比較早,通常是在App啟動(dòng)加載的時(shí)候開始,這時(shí)候并不能保證所有的類都被加載完成并且可以使用。并且load加載自身也存在不確定性,因?yàn)樵谟幸蕾囮P(guān)系的兩個(gè)庫(kù)中,被依賴的類的load方法會(huì)先調(diào)用,但是在一個(gè)庫(kù)之內(nèi)調(diào)用的順序是不確定的。除此之外,load方法是線程安全的,因?yàn)閮?nèi)部實(shí)現(xiàn)加上了鎖,但是也帶來(lái)了一定的性能開銷,所以不適合處理很復(fù)雜的事情。一般,會(huì)在load方法實(shí)現(xiàn)Method Swizzle(方法交換實(shí)現(xiàn))。
4.load函數(shù)的實(shí)現(xiàn)原理
在分析load函數(shù)實(shí)現(xiàn)原理之前,我們先打個(gè)斷點(diǎn),看一下load函數(shù)的加載過(guò)程,斷點(diǎn)如下:

運(yùn)行時(shí),我們看一下函數(shù)調(diào)用棧:

從調(diào)用棧中可以看得出
load_image函數(shù)開始加載,在call_load_methods調(diào)用所有類的load方法。打開runtime源碼,這里下載的是最新的objc4-709,打開objc-runtime-new.mm文件,找到load_image函數(shù):
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
我們可以看到,調(diào)用了prepare_load_methods()函數(shù),提前準(zhǔn)備需要調(diào)用的所欲load函數(shù),看一下prepare_load_methods()函數(shù)內(nèi)部實(shí)現(xiàn),只是列出的關(guān)鍵代碼:
void prepare_load_methods(const headerType *mhdr)
{
Module mods;
unsigned int midx;
header_info *hi;
...
// Major loop - process all modules in the image
mods = hi->mod_ptr;
for (midx = 0; midx < hi->mod_count; midx += 1)
{
unsigned int index;
...
// Minor loop - process all the classes in given module
for (index = 0; index < mods[midx].symtab->cls_def_cnt; index += 1)
{
// Locate the class description pointer
Class cls = (Class)mods[midx].symtab->defs[index];
if (cls->info & CLS_CONNECTED) {
schedule_class_load(cls);
}
}
}
// Major loop - process all modules in the header
mods = hi->mod_ptr;
// NOTE: The module and category lists are traversed backwards
// to preserve the pre-10.4 processing order. Changing the order
// would have a small chance of introducing binary compatibility bugs.
midx = (unsigned int)hi->mod_count;
while (midx-- > 0) {
unsigned int index;
unsigned int total;
Symtab symtab = mods[midx].symtab;
...
// Minor loop - register all categories from given module
index = total;
while (index-- > mods[midx].symtab->cls_def_cnt) {
old_category *cat = (old_category *)symtab->defs[index];
add_category_to_loadable_list((Category)cat);
}
}
}
prepare_load_methods()函數(shù)里面首先獲取的是class的load方法,最后才獲取module里所有類的category的load方法,所以class和category中l(wèi)oad函數(shù)記載的順序是:class -> category?,F(xiàn)在,我們來(lái)看schedule_class_load()函數(shù):
static void schedule_class_load(Class cls)
{
if (cls->info & CLS_LOADED) return;
if (cls->superclass) schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->info |= CLS_LOADED;
}
這個(gè)方法是一個(gè)遞歸函數(shù),保證父類先被添加進(jìn)列表,然后才是類本身,所以superClass和class的load方法加載順序是:superClass -> class。下面,我們看一下add_class_to_loadable_list()函數(shù)做了一些什么事:
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
loadable_classes_used是已經(jīng)使用的內(nèi)存,loadable_classes_allocated是分配的內(nèi)存,loadable_classes則是一個(gè)全局的結(jié)構(gòu)體,存放模塊里所有的class的load函數(shù)。很明顯這個(gè)方法就是將所有的load函數(shù)加入loadable_classes結(jié)構(gòu)體存放。關(guān)于category的處理也一樣,將所有的load函數(shù)存放在loadable_categories全局的結(jié)構(gòu)體里。load函數(shù)加載完了,我們看一下load函數(shù)的調(diào)用,查找call_load_methods()函數(shù):
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
關(guān)鍵的方法call_class_loads()和call_category_loads(),下面看一下call_class_loads()函數(shù)的實(shí)現(xiàn):
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
上面代碼其實(shí)就是從loadable_classes把load函數(shù)取出來(lái),然后調(diào)用。值得注意的是 (*load_method)(cls, SEL_load),load方法是直接使用函數(shù)指針調(diào)用,也就是走C語(yǔ)言函數(shù)調(diào)用的流程,不是發(fā)送消息,并不會(huì)走消息轉(zhuǎn)發(fā)的流程,也就是說(shuō),如果一個(gè)類實(shí)現(xiàn)了load函數(shù)就會(huì)調(diào)用,如果沒(méi)有實(shí)現(xiàn)也不會(huì)調(diào)用該類的父類load函數(shù)實(shí)現(xiàn),如果父類實(shí)現(xiàn)了load函數(shù)的話。category調(diào)用load方法也是一樣的道理。
initialize
1. initialize函數(shù)的加載時(shí)機(jī)
蘋果官網(wǎng)描述:
Initializes the class before it receives its first message.
這意味著名,這個(gè)函數(shù)是懶加載,只有當(dāng)類接收了第一個(gè)消息的時(shí)候才會(huì)調(diào)用initialize函數(shù),否則一直不會(huì)調(diào)用。
2.initialize函數(shù)的調(diào)用順序
來(lái)自蘋果官網(wǎng)的描述:
Superclasses receive this message before their subclasses.
The superclass implementation may be called multiple times if subclasses do not implement initialize.
initialize函數(shù)的調(diào)用順序?yàn)椋簊uperClass -> class。這里沒(méi)有分類,因?yàn)橐粋€(gè)類的initialize函數(shù)只會(huì)調(diào)用一次,如果需要實(shí)現(xiàn)獨(dú)立的class和category的初始化就需要實(shí)現(xiàn)load函數(shù)。還需要注意的一點(diǎn)就是,如果subClass沒(méi)有實(shí)現(xiàn)initialize函數(shù),則父類的initialize函數(shù)會(huì)被調(diào)用兩次,代碼如下:
//ClassA
@implementation ClassA
+ (void)initialize {
NSLog(@"initial_class_a");
}
@end
//ClassASub
@implementation ClassASub
+ (void)initialize {
NSLog(@"initial_class_a_sub");
}
@end
//ClassA (Addition)
@implementation ClassA (Addition)
+ (void)initialize {
NSLog(@"initial_class_a_addition");
}
@end
//ClassB
@implementation ClassB
+ (void)initialize {
NSLog(@"initial_class_b");
}
@end
//ClassBSub
@implementation ClassBSub
@end
//result
2017-04-09 16:15:21.919597+0800 BasicTest[44479:3187122] initial_class_a_addition
2017-04-09 16:15:21.919742+0800 BasicTest[44479:3187122] initial_class_a_sub
2017-04-09 16:15:21.919756+0800 BasicTest[44479:3187122] initial_class_b
2017-04-09 16:15:21.919763+0800 BasicTest[44479:3187122] initial_class_b
由于initialize函數(shù)可能會(huì)被調(diào)用多次,所以,如果想保證initialize函數(shù)只被調(diào)用一次,蘋果建議這樣做:
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}
3.initialize函數(shù)的使用場(chǎng)景
蘋果官方文檔:
The runtime sends the initialize message to classes in a thread-safe manner.
That is, initialize is run by the first thread to send a message to a class, and any other thread that tries to send a message to that class will block until initialize completes.
Because initialize is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible.
Specifically, any code that takes locks that might be required by other classes in their initialize methods is liable to lead to deadlocks.
Therefore, you should not rely on initialize for complex initialization, and should instead limit it to straightforward, class local initialization.
initialize是線程安全的,有可能阻塞線程,所以,initialize函數(shù)應(yīng)該限制做一些簡(jiǎn)單,不復(fù)雜的類初始化的前期準(zhǔn)備工作。
4.initialize函數(shù)實(shí)現(xiàn)原理
我們先給ClassA的initialize函數(shù)打上斷點(diǎn)標(biāo)志,如下:

函數(shù)調(diào)用棧如下:

從函數(shù)調(diào)用??梢钥闯?,在調(diào)用
_class_initialize函數(shù)之前,調(diào)用了lookUpImpOrForward函數(shù),我們看一下lookUpImpOrForward函數(shù)的實(shí)現(xiàn):
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
...
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
...
}
在該函數(shù)里面調(diào)用了_class_initialize函數(shù),看一下該函數(shù)的內(nèi)部實(shí)現(xiàn):
void _class_initialize(Class cls)
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
...
if (reallyInitialize) {
callInitialize(cls);
}
...
}
_class_initialize函數(shù)中調(diào)用了callInitialize函數(shù),其中調(diào)用順序是從父類開始,到子類的??匆幌?code>callInitialize函數(shù)做了什么:
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
callInitialize函數(shù)的工作相當(dāng)簡(jiǎn)單,就是發(fā)送消息,這是和load函數(shù)實(shí)現(xiàn)不一樣的地方,load函數(shù)的調(diào)用直接是函數(shù)指針的調(diào)用,而initialize函數(shù)是消息的轉(zhuǎn)發(fā)。所以,class的子類就算沒(méi)有實(shí)現(xiàn)initialize函數(shù),也會(huì)調(diào)用父類的initialize函數(shù),如果子類實(shí)現(xiàn)了initialize函數(shù),則子類不會(huì)調(diào)用父類的initialize函數(shù)。
總結(jié)
通過(guò)分別對(duì)load和initialize源代碼的實(shí)現(xiàn)細(xì)節(jié),我們知道了它們各自的特點(diǎn),總的如下:
1.load在被添加到runtime的時(shí)候加載,initialize是類第一次收到消息的時(shí)候被加載,load是在main函數(shù)之前,initialize是在main函數(shù)之后。
2.load方法的調(diào)用順序是:superClass -> class -> category;initialize方法的調(diào)用順序是:superClass -> class。都不需要顯示調(diào)用父類的方法,系統(tǒng)會(huì)自動(dòng)調(diào)用,load方法是函數(shù)指針調(diào)用,initialize是發(fā)送消息。子類如果沒(méi)有實(shí)現(xiàn)load函數(shù),子類是不會(huì)調(diào)用父類的load函數(shù)的,但是子類沒(méi)有實(shí)現(xiàn)initialize函數(shù),則會(huì)調(diào)用父類的initialize函數(shù)。
3.load和initialize內(nèi)部實(shí)現(xiàn)都加了線程鎖,是線程安全的,因此,這兩個(gè)函數(shù)應(yīng)該做一些簡(jiǎn)單的工作,不適合復(fù)雜的工作。
4.load函數(shù)通常用來(lái)進(jìn)行Method Swizzle,initialize函數(shù)則通常初始化一些全局變量,靜態(tài)變量。
參考:
細(xì)說(shuō)OC中的load和initialize方法
load
initialize
Objective-C +load vs +initialize
Objective-C 深入理解 +load 和 +initialize
NSObject +load and +initialize - What do they do?
Notification Once
objc-runtime-new
iOS 程序 main 函數(shù)之前發(fā)生了什么
深入理解iOS App的啟動(dòng)過(guò)程