Category-load、initialize調用原理

面試題

load、initialize方法的區別是什么?他們在Category中的調用順序?

load調用原理

1.+load方法會在runtime加載類、分類的時候調用,系統會主動調用

2.每個類、分類的+load,在程序運行中只會調用一次

3.調用順序
1>先調用父類的+load
a)按照編譯順序先后調用(先編譯,先調用)
b)調用子類的+load之前會先調用父類的+load

2>再調用分類的+load
a)按照編譯先后順序調用(先編譯,先調用)

首先給出結論,接下來通過代碼驗證和源碼分析。

load代碼驗證

先來一段代碼,分析load方法的調用情況。
創建Person類繼承至NSObject,Person+Test1分類,Person+Test2分類;再創建Student類繼承至Person,Student+Test1分類,Person+Test2分類;最后創建Dog也是繼承至NSObject,與Person類對照。

@interface Person : NSObject

@end

@implementation Person

+ (void)load{
    NSLog(@"Person +load");
}

@end



@interface Person (Test1)

@end

@implementation Person (Test1)

+ (void)load{
    NSLog(@"Person (Test1) +load");
}

@end



@interface Person (Test2)

@end

@implementation Person (Test2)

+ (void)load{
    NSLog(@"Person (Test2) +load");
}

@end



@interface Student : Person

@end

@implementation Student

+ (void)load{
    NSLog(@"Student +load");
}

@end



@interface Student (Test1)

@end

@implementation Student (Test1)

+ (void)load{
    NSLog(@"Student (Test1) +load");
}

@end



@interface Student (Test2)

@end

@implementation Student (Test2)

+ (void)load{
    NSLog(@"Student (Test1) +load");
}

@end


@interface Dog : NSObject

@end

+ (void)load{
    NSLog(@"Dog +load");
}

運行上面代碼,在外部不調用Person,Student,Dog中的方法,每個.m文件中+load都調用了一遍。運行結果如下圖。


+load調用順序

代碼主要驗證下load方法的調用順序
1>只在Person.m、Person+Test1.m、Person+Test2.m中實現+load方法,其他.m文件中的+load方法都屏蔽掉,觀察調用類和分類的+load方法順序。


調用類和分類中+load順序

運行結果顯示先調用類的+load,再調用分類的+load。

2>只在Person.m、Student.m、Dog.m中實現+load方法,其他.m文件中的+load方法都屏蔽掉,觀察沒有分類時,調用類的+load方法順序。


調用類中+load順序

上圖中可以看出文件的編譯順序是Dog.m->Student.m->Person.m,其中Student繼承至Person。而運行結果Dog中+load調用先于Person,說明調用類中+load方法是按照編譯順序調用,先編譯先調用。Student的編譯順序先于Person,為什么調用順序反而在后面呢?這是因為Student繼承至Person,調用子類的+load前會先調用父類的+load。

3>只在Persson+Test1.m、Persson+Test2.m、Student+Test1.m、Student +Test2.m中實現+load方法,其他.m文件中的load方法都屏蔽掉,觀察調用分類的+load方法順序。


調用分類中+load順序

上圖中可以看出分類文件的編譯順序是Person+Test1.m->Person+Test2.m->Student+Test1.m-> Student +Test2.m,而運行的結果和編譯順序是一樣的。說明調用分類的+load是按照編譯順序,先編譯先調用。

從上面的三步分別驗證,再看第一次運行的結果截圖,這個順序是完全符合的。這樣也就驗證了最開頭的+load調用順序的總結。并且在外部完全不調用+load方法的時候,+load方法依然會被調用,其實就是runtime在加載類和分類的時候就主動調用了+load方法,同時結合以上運行結果,程序運行過程中只會調用一次+load方法。

load源碼分析

為什么調用+load會出現以上的規律呢?我們通過runtime源碼來一探究竟。

先貼一個源碼解析的流程圖


+load源碼解析流程

首先來到runtime的初始化方法,在objc-os.mm中搜索_objc_init

runtime初始化函數

再來到load_images,這個函數中主動調用了load方法。

load_images函數

我們先看看系統是怎么去查找load方法的,進入到prepare_load_methods函數。

prepare_load_methods函數.png

這里發現類和分類都是分別按照編譯的順序取出來,分類取出來之后就直接按編譯順序放到了一個loadable_list中,而類取出來中又調用了schedule_class_load函數,在這個函數中其實是給類和父類調用順序排序。

schedule_class_load函數

上圖可以看出,每個類中的+load方法都只會調用一次,遞歸的將類和父類都添加到loadable_list中,并且父類會排在前面。

接下來再看看add_class_to_loadable_listadd_category_to_loadable_list中具體做了什么

add_class_to_loadable_list函數和add_category_to_loadable_list函數

查找load方法的邏輯總結(prepare_load_methods)

類和其對應的load方法,賦值給loadable_class,最后統一添加到loadable_classes中
順序是按文件編譯的順序,但是父類會強制排在子類前面,并且每個類只會被添加一次

分類和其對應的load方法,賦值給loadable_category,最后統一添加到loadable_categories中
順序就是按編譯的順序

接著在來看call_load_methods,調用load方法邏輯。

call_load_methods函數

這里就可以發現在調用load方法時,是優先調用類的+load方法,再調用分類的+load方法。

call_class_loadscall_category_loads中具體如何執行的,我們繼續向下看。

call_class_loads函數

call_category_loads函數

在類和分類中都是直接找到+load方法然后調用。所以不存在先調用調用子類的+load,就不調用父類的+load,也不存在先調用分類的+load,就不調用原本類中的+load。類和分類中的+load都會在runtime初始化時主動被系統調用,并且在運行過程中只調用一次。

initialize調用原理

1.+initialize方法會在類第一次接收到消息時調用

2.調用順序
a)先調用父類的+initialize,再調用子類的+initialize(先初始化父類,再初始化子類,每個類只會初始化一次)

+initialize是通過objc_msgSend進行調用的,所以有以下特點
a)如果子類沒有實現+initialize,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)
b)如果分類實現了+initialize,就會覆蓋類本身的+initialize調用

接下來通過代碼驗證和源碼分析。

代碼驗證
@interface Person : NSObject

@end

@implementation Person

+ (void)initialize{
    NSLog(@"Person +initialize");
}

@end


@interface Person (Test1)

@end

@implementation Person (Test1)

+ (void)initialize{
    NSLog(@"Person (Test1) +initialize");
}

@end


@interface Person (Test2)

@end

@implementation Person (Test2)

+ (void)initialize{
    NSLog(@"Person (Test2) +initialize");
}

@end


@interface Student : Person

@end

@implementation Student

+ (void)initialize{
    NSLog(@"Student +initialize");
}

@end


@interface Student (Test1)

@end

@implementation Student (Test1)

+ (void)initialize{
    NSLog(@"Student (Test1) +initialize");
}

@end


@interface Student (Test2)

@end

@implementation Student (Test2)

+ (void)initialize{
    NSLog(@"Student (Test1) +initialize");
}

@end


@interface Dog : NSObject

@end

@implementation Dog

+ (void)initialize{
    NSLog(@"Dog +initialize");
}

@end

1>以上代碼,在外部不調用所有類和分類,運行結果是沒有調用任何一個+initialize方法。

2.0>只在Person.m、Student.m、Dog.m中實現+initialize方法,其他.m文件中的+initialize方法都屏蔽掉,在外部調用[Person alloc];[Student alloc]; [Dog alloc];,分別給Person類發送了alloc,給Student類發送了alloc,給Dog類發送了alloc消息,觀察運行結果。

2.1>在外部調用[Person alloc];[Student alloc]; [Dog alloc];[Person alloc];[Student alloc]; [Dog alloc],多次分別給Person類發送了alloc,給Student類發送了alloc,給Dog類發送了alloc消息,與2.0作為對照,觀察運行結果。

2.2>在外部調用[Student alloc];[Person alloc];[Dog alloc];,調換Student和Person發送alloc消息的順序,同樣與2.0作為對照,觀察運行結果。

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Student.h"
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //2.0
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        //2.1
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        [Person alloc];
        [Student alloc];
        [Dog alloc];
        
        //2.2
        [Student alloc];
        [Person alloc];
        [Dog alloc];
        
    }
    return 0;
}

運行結果
2019-07-11 20:58:54.031975+0800 Category-initialize[12113:4824617] Person +initialize
2019-07-11 20:58:54.032140+0800 Category-initialize[12113:4824617] Student +initialize
2019-07-11 20:58:54.032152+0800 Category-initialize[12113:4824617] Dog +initialize

以上2.0,2.1,2.2三種運行結果都是一致。
通過以上四種情況的結果,說明+initialize方法會在類第一次接收到消息的時候調用。并且會先調用父類的+initialize,再調用子類的+initialize。

3>只在Person.m中實現+initialize方法,其他所有.m中的+initialize方法都屏蔽,包括Person的子類Student。在外部調用[Student alloc];,觀察運行結果。

2019-07-11 21:28:06.730804+0800 Category-initialize[12336:4875791] Person +initialize
2019-07-11 21:28:06.730959+0800 Category-initialize[12336:4875791] Person +initialize

結果是調用了兩次父類Person中的+int initialize,進一步說明先調用父類的+initialize,再調用子類的+initialize,同時子類沒有實現+initialize,會調用父類的+initialize。

4>每個.m中都實現+initialize,在外部調用[Person alloc];[Student alloc]; [Dog alloc];,觀察運行結果。


+initialize調用順序

結果調用了Person+Test1和Student+Test1中的+initialize方法。也說明了如果分類實現了+initialize,就覆蓋類本身的+initialize調用。而多個分類中的調用順序是,后編譯先調用,都是符合的。

initialize源碼分析

為什么調用+initialize會出現以上的規律呢?我們也通過runtime源碼來一探究竟。

先貼一個源碼解析的流程圖


+initialize源碼解析流程

因為+initialize是在類第一次接收到消息時調用,那底層一定是調用了objc_msgSend,相當于objc_msgSend(cls,@selector(@"alloc"))給類cls發送了一條alloc消息。 在runtime源碼中搜索objc_msgSend,結果在objc-msg-arm64.s中發現其是通過匯編實現的。無法看懂匯編的情況下我們只能先行分析,發送消息,通過isa找到類,然后要經歷查找方法和調用方法兩個步驟,而+initialize就可能是在這兩個過程中調用的。

我們通過XCode斷點alloc方法,然后顯示匯編來查看匯編中查找方法和調用方法的流程。步驟Debug->Debug workflow->Always Show Disassembly,找到callq-msgSend并且斷點,跳到斷點處,control+stepinto進入到實現內部,發現最后回來到_objc_msgSend_uncached,斷點并跳到此處,control+stepinto進入到實現內部,我們終于找到了一個不是匯編的函數_class_lookupMethodAndLoadCache3,在runtime源碼中搜索找到該方法在objc-runtime-new.mm中,我們就來順著這個方法看看內部的實現。

_class_lookupMethodAndLoadCache3函數

接下來進入lookUpImpOrForward函數內部。

lookUpImpOrForward函數

再接著進入到_class_initialize函數內部。

_class_initialize函數

最后來到callInitialize函數內部,發現+initialize就是通過objc_msgSend進行調用的。

callInitialize函數

結合底層源碼,也都一一驗證了關于+initialize調用原理的總結。為什么是在類第一次收到消息時調用?為什么調用子類的+initialize會先調用父類的+initialize?以及+initialize調用的兩個特點,都能得到解答。

接下來進行面試題的總結。

load、initialize方法的區別是什么?他們在Category中的調用順序?
1.調用方式
1>load是根據函數地址調用
2>initialize是通過objc_msgSend調用

2.調用時刻
1>load是runtime加載類、分類的時候調用(只會調用一次)
2>initialize是類第一次接收到消息的時候調用,每一個類只會initialize一次(父類的initialize方法可能會調用多次)

load、initialize調用順序
1.load
1>先調用類的load
a)先編譯的類,優先調用load
b)調用子類的load之前,會優先調用父類的load

2>再調用分類的load
a)先編譯的分類,優先調用load

2.intialize
1>先初始化父類
2>再初始化子類(可能最終調用的是父類的initialize方法)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容