面試題1:Category中有load方法嗎?load方法是什麼時候呼叫? 面試題2:load,initialize的區別是什麼?它們在Category中的呼叫順序以及出現繼承時它們之間的呼叫過程是怎麼樣的? 那麼這篇文章主要就是回答這兩個問題。
load方法
load方法什麼時候呼叫? load方法是在runtime載入類和分類的時候呼叫。 我們建立了一個Person類和它的兩個分類,然後重寫了各自的load方法:
//Person
+ (void)load{
NSLog(@"Person + load");
}
//Person+Test1
+ (void)load{
NSLog(@"Person (Test1) + load");
}
//Person+Test2
+ (void)load{
NSLog(@"Person (Test2) + load");
}
複製程式碼
然後我們什麼也不做,執行程式碼,看到列印結果:
2018-07-24 20:45:08.369170+0800 interview - Category[14157:409819] Person + load
2018-07-24 20:45:08.371806+0800 interview - Category[14157:409819] Person (Test1) + load
2018-07-24 20:45:08.373190+0800 interview - Category[14157:409819] Person (Test2) + load
複製程式碼
通過列印結果我們可以看到Person及其分類的load方法都被呼叫了,這就證實了load方法是由runtime載入類和分類的時候呼叫的。
然後我們再給Person類及其子類建立一個+ (void)test
方法並實現它:
//Person
+ (void)test{
NSLog(@"Person + test");
}
//Person+Test1
+ (void)test{
NSLog(@"Person (Test1) + test");
}
//Person+Test2
+ (void)test{
NSLog(@"Person (Test2) + test");
}
複製程式碼
然後用Person類物件去呼叫test方法:
[Person test];
複製程式碼
得到列印結果:
2018-07-24 21:07:32.886316+0800 interview - Category[14670:428685] Person + load
2018-07-24 21:07:32.887195+0800 interview - Category[14670:428685] Person (Test1) + load
2018-07-24 21:07:32.887461+0800 interview - Category[14670:428685] Person (Test2) + load
2018-07-24 21:07:33.050735+0800 interview - Category[14670:428685] Person (Test2) + test
複製程式碼
通過列印結果我們可以看到,Person (Test2)的test方法被呼叫了,這個很好理解因為我們在Category的本質<一>中說的很清楚了,如果分類和類同時實現了一個方法,那麼分類中的方法和類中的方法都會儲存下來存入記憶體中,並且分類的方法在前,類的方法在後,這樣在呼叫的時候就會首先找到分類的方法,給人的感覺就是好像類的方法被覆蓋了。 那麼問題來了,同樣是類方法,同樣是分類中實現了類的方法,為什麼load方法不像test方法一樣,呼叫分類的實現,而是類和每個分類中的load方法都被呼叫了呢?load方法到底有什麼不同呢? 要想弄清楚其中的原理,我們還是要從runtime的原始碼入手:
- 1.找到objc-os.mm這個檔案,然後找到這個檔案的
void _objc_init(void)
這個方法,runtime的初始化都是在這個方法裡面完成。 - 2.這個方法的最後一行呼叫了函式
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
,我們點進load_images,這是載入模組的意思。 - 3.
- 4
- 5我們點進
call_class_loads();
這個方法檢視對類的load方法的呼叫過程: - 6.然後我們再點進
call_category_loads()
檢視對分類的load方法的呼叫過程: 那麼這樣我們就搞清楚了為什麼load方法不是像test方法一樣,執行分類的實現
因為load方法的呼叫並不是objc_msgSend機制,它是直接找到類的load方法的地址,然後呼叫類的load方法,然後再找到分類的load方法的地址,再去呼叫它。
而test方法是通過訊息機制去呼叫的。首先找到類物件,由於test方法是類方法,儲存在元類物件中,所以通過類物件的isa指標找到元類物件,然後在元類物件中尋找test方法,由於分類也實現了test方法,所以分類的test方法是在類的test方法的前面,首先找到了分類的test方法,然後去呼叫它。
有繼承關係時load方法的呼叫順序
通過上面的分析我們確定了load方法的一個呼叫規則:先呼叫所有類的load方法,然後再呼叫所有分類的load方法。
下面我們再建立一個Student類繼承自Person類,並且為Student類建立兩個子類Student (Test1), Student (Test2),並且覆寫load方法:
//Student
+ (void)load{
NSLog(@"Student + load");
}
//Student (Test1)
+ (void)load{
NSLog(@"Student (Test1) + load");
}
//Student (Test2)
+ (void)load{
NSLog(@"Student (Test2) + load");
}
複製程式碼
然後我們執行一下程式,看列印結果:
2018-07-25 15:45:58.605156+0800 interview - Category[13869:359239] Person + load
2018-07-25 15:45:58.605684+0800 interview - Category[13869:359239] Student + load
2018-07-25 15:45:58.606420+0800 interview - Category[13869:359239] Student (Test2) + load
2018-07-25 15:45:58.606870+0800 interview - Category[13869:359239] Person (Test1) + load
2018-07-25 15:45:58.607293+0800 interview - Category[13869:359239] Student (Test1) + load
2018-07-25 15:45:58.607514+0800 interview - Category[13869:359239] Person (Test2) + load
2018-07-25 15:45:58.812025+0800 interview - Category[13869:359239] Person (Test2) + test
複製程式碼
通過列印結果我們可以很清楚的看見,Person類和Student類的load方法先被呼叫,然後呼叫分類的load方法。再執行多次,都是Person類和Student類的load方法先被呼叫,然後分類的方法才被呼叫。並且總是Person類的load在Student類的load方法前面被呼叫,這會不會和編譯順序有關呢?我們改變一下編譯順序看看:
目前是Person類在Student類的前面編譯,現在我們把Student類放到Person類的前面編譯: 然後我們再執行一下程式,檢視列印結果:TARGETS -> Build Phases -> Complle Sources中檔案的放置順序就是檔案的編譯順序。
2018-07-25 15:56:07.270034+0800 interview - Category[14070:367686] Person + load
2018-07-25 15:56:07.270619+0800 interview - Category[14070:367686] Student + load
2018-07-25 15:56:07.271107+0800 interview - Category[14070:367686] Student (Test2) + load
2018-07-25 15:56:07.271494+0800 interview - Category[14070:367686] Person (Test1) + load
2018-07-25 15:56:07.271762+0800 interview - Category[14070:367686] Student (Test1) + load
2018-07-25 15:56:07.272118+0800 interview - Category[14070:367686] Person (Test2) + load
2018-07-25 15:56:07.433068+0800 interview - Category[14070:367686] Person (Test2) + test
複製程式碼
我們發現還是Person類的load方法在Student類前面被呼叫,所以好像和編譯順序無關呀。那麼我們就需要思考一下是不是由於Student和Person之間的繼承關係導致的呢? 為了搞清楚這個問題,我們只能從runtime的原始碼入手。
- 1.objc-os.mm中
void _objc_init(void)
這個入口方法,點進load_images. - 2.在
void load_images(const char *path __unused, const struct mach_header *mh)
這個方法中,最後有個call_load_methods();
方法,點選進去。 - 3.在
void call_load_methods(void)
這個方法中,找到call_class_loads();
這個方法,上面已經講到,這是呼叫類的load方法。點進去。 - 4
- 5.為了搞清楚這裡的classes陣列的來歷,我們回退到
void load_images(const char *path __unused, const struct mach_header *mh)
這個方法,這個方法中有一個prepare_load_methods((const headerType *)mh);
這個方法,根據方法名可能和我們的問題有關。因此我們點進這個方法檢視一下 - 6.
- 7.點進
schedule_class_load(remapClass(classlist[i]));
複製程式碼
這個方法:
通過這個方法我們就可以很清晰的看到,當要把一個類加入最終的這個classes陣列的時候,會先去上溯這個類的父類,先把父類加入這個陣列。 由於在classes陣列中父類永遠在子類的前面,所以在載入類的load方法時一定是先載入父類的load方法,再載入子類的load方法。
類的load方法呼叫順序搞清楚了我們再來看一下分類的load方法呼叫順序
我們還是看一下void prepare_load_methods(const headerType *mhdr)
這個函式
通過這個分析我們就能知道,分類的load方法載入順序很簡單,就是誰先編譯的,誰的load方法就被先載入。
下面我們通過列印結果驗證一下,這是編譯順序:
按照我們前面的分析,load方法的呼叫順序應該是: Person -> Student -> Person + Test1 -> Student + Test2 -> Student + Test1 -> Person + Test2。 我們看一下列印結果:2018-07-25 16:48:10.271679+0800 interview - Category[15094:408222] Person + load
2018-07-25 16:48:10.272357+0800 interview - Category[15094:408222] Student + load
2018-07-25 16:48:10.272661+0800 interview - Category[15094:408222] Person (Test1) + load
2018-07-25 16:48:10.272872+0800 interview - Category[15094:408222] Student (Test2) + load
2018-07-25 16:48:10.273103+0800 interview - Category[15094:408222] Student (Test1) + load
2018-07-25 16:48:10.273434+0800 interview - Category[15094:408222] Person (Test2) + load
2018-07-25 16:48:10.441457+0800 interview - Category[15094:408222] Person (Test2) + test
複製程式碼
列印結果完美的驗證了我們的結論。
總結 load方法呼叫順序
1.先呼叫類的load方法
- 按照編譯先後順序呼叫(先編譯,先呼叫)
- 呼叫子類的load方法之前會先呼叫父類的load方法
2.再呼叫分類的load方法
- 按照編譯先後順序,先編譯,先呼叫
initialize方法
initialize方法的呼叫時機
- initialize在類第一次接收到訊息時呼叫,也就是objc_msgSend()。
- 先呼叫父類的+initialize,再呼叫父類的initialize。 我們首先給Student類和Person類覆寫+initialize方法:
//Person
+ (void)initialize{
NSLog(@"Person + initialize");
}
//Person+Test1
+ (void)initialize{
NSLog(@"Person (Test1) + initialize");
}
//Person+Test2
+ (void)initialize{
NSLog(@"Person (Test2) + initialize");
}
//Student
+ (void)initialize{
NSLog(@"Student + initialize");
}
//Student (Test1)
+ (void)initialize{
NSLog(@"Student (Test1) + initialize");
}
//Student (Test2)
+ (void)initialize{
NSLog(@"Student (Test2) + initialize");
}
複製程式碼
我們執行程式,發現什麼也沒有列印,說明在執行期沒有呼叫+initialize方法。 然後我們給Person類傳送訊息,也就是呼叫函式:
[Person alloc];
複製程式碼
列印結果:
2018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize
複製程式碼
可以看到呼叫了Person類的分類的initialize方法。通過這個列印結果我們能看出initialize方法和load方法的不同,load方法由於是直接獲取方法的地址,然後呼叫方法,所以Person及其分類的load方法都會呼叫。而initialize方法則更像是通過訊息機制,也即是objc_msgend(Person, @selector(initialize))這種來呼叫的。 然後我多次呼叫alloc方法:
[Person alloc];
[Person alloc];
[Person alloc];
複製程式碼
列印結果:
018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize
複製程式碼
可見initialize方法只在類第一次收到訊息時呼叫。然後我們再給Student類傳送訊息:
[Student alloc];
複製程式碼
列印結果:
2018-07-25 18:34:14.648279+0800 interview - Category[17187:473502] Person (Test2) + initialize
2018-07-25 18:34:14.648394+0800 interview - Category[17187:473502] Student (Test1) + initialize
複製程式碼
我們看到不僅呼叫了Student類的initialize方法,而且還呼叫了Student類的父類,Person類的方法,因此我們猜測在呼叫類的initialize方法之前會先呼叫父類的initialize方法。
以上僅僅是我們根據列印結果的猜測,還需要通過原始碼來驗證。
[Person alloc]
就相當於objc_msgSend([Person class], @selector(alloc))
,說明objc_msgSend()內部會去呼叫initialize方法,判斷是第幾次接收到訊息。
- 1.我們去runtime原始碼中搜尋
class_getClassmethod
方法,會在objc-class.mm這個檔案中找到這個方法的實現: - 2.我們點進
class_getInstanceMethod(cls->getMeta(), sel);
這個方法: - 3.點進這個方法:
- 4.繼續尋找
lookUpImpOrForward
這個方法的實現,我擷取其中有價值的程式碼塊:這個程式碼也說明了每個類的+initialize方法只會被呼叫一次。 - 5.我們點進
_class_initialize (_class_getNonMetaClass(cls, inst));
尋找真正的實現: - 6.然後我們通過
callInitialize(cls);
檢視具體的調 這樣一來+initialize方法的呼叫過程就很清楚了。
+initialize的呼叫過程:
- 1檢視本類的initialize方法有沒有實現過,如果已經實現過就返回,不再實現。
- 2.如果本類沒有實現過initialize方法,那麼就去遞迴檢視該類的父類有沒有實現過initialize方法,如果沒有實現就去實現,最後實現本類的initialize方法。並且initialize方法是通過objc_msgSend()實現的。
+initialize和+load的一個很大區別是,+initialize是通過objc_msgSend進行呼叫的,所以有以下特點:
- 如果子類沒有實現+initialize方法,會呼叫父類的+initialize(所以父類的+initialize方法可能會被呼叫多次)
- 如果分類實現了+initialize,會覆蓋類本身的+initialize呼叫。
下面我們把Student類及其分類中的+initialize這個方法的實現去掉,然後增加一個Teacher類繼承自Person類。然後我們給Student類和Teacher類都傳送alloc訊息:
[Student alloc];
[Teacher alloc];
複製程式碼
這個時候也就是隻有Person類及其分類實現了+initialize方法。那麼列印結果會是怎樣呢?
2018-07-25 21:47:59.899995+0800 interview - Category[20981:582224] Person (Test2) + initialize
2018-07-25 21:47:59.900112+0800 interview - Category[20981:582224] Person (Test2) + initialize
2018-07-25 21:47:59.900240+0800 interview - Category[20981:582224] Person (Test2) + initialize
複製程式碼
這裡Person類的+initialize方法竟然被呼叫了三次,這多少有些出乎意外吧。下面我們來分析一下。
BOOL studentInitialized = NO;
BOOL personinitialized = NO;
BOOL teacherInitialized = NO;
[Student alloc];
//判斷Student類是否初始化了,這裡Student類還沒有被初始化,所以進入條件語句。
if(!studentInitialized){
//判斷Student類的父類Person類是否初始化了
if(!personinitialized){
//這裡Person類還沒有初始化,就利用objc_msgSend呼叫initialize方法
objc_msgSend([Person class], @selector(initialize));
//變更Person類是否初始化的狀態
personinitialized = YES;
}
//利用objc_msgSend呼叫Student的initialize方法
objc_msgSend([Student class], @selector(initialize));
//變更Student是否初始化的狀態
studentInitialized = YES
}
[Teacher alloc];
//判斷Teacher類是否已經初始化了,這裡Teacher類還沒有初始化,進入條件語句
if(!teacherInitialized){
//判斷其父類Person類是否初始化了,這裡父類已經初始化了,所以不會進入這個條件語句
if(!personinitialized){
objc_msgSend([Person class], @selector(initialize));
personinitialized = YES;
}
//利用objc_msgSend呼叫Teacher類的initialize方法
objc_msgSend([Teacher class], @selector(initialize));
//變更狀態
teacherInitialized = YES;
}
複製程式碼
上面列出來的是呼叫initialize的虛擬碼,下面再詳細說明這個過程:
- 1.Student類收到alloc訊息,開始著手準備呼叫initialize方法。首先判斷自己有沒有初始化過。
- 2.判斷自己沒有初始化過,所以就去找自己的父類Person類,看Person類有沒有初始化過,發現Person類也沒有初始化過,且Person類也沒有父類,多以對Person類使用
objc_msgSend([Person class], @selector(initialize))
呼叫Person類的initialize方法。這是第一次呼叫Person類的initialize方法。 - 3.父類處理完後,再通過
objc_msgSend([Student class], @selector(initialize));
呼叫Student類的initialize方法,但是由於Student類沒有實現initialize方法,所以通過其superclass指標找到父類Person類,然後呼叫了Person類的initialize實現。這是第二次呼叫Person類的initialize方法。 - 4.Teacher類收到alloc方法,開始準備呼叫initialize放啊發。首先判斷自己有沒有被初始化過。
- 5.判斷自己沒有被初始化過後,又開始判斷其父類Person類有沒有被初始化過,剛剛父類Person類已經被初始化過。
- 6.於是通過
objc_msgSend([Teacher class], @selector(initialize))
呼叫Teacher類的initialize方法。但是由於Teacher類沒有實現initialize方法,所以只能通過superclass指標去查詢父類有沒有實現initialize方法,發現父類Person類實現了initialize方法,於是呼叫父類的initialize方法。這是第三次呼叫Person類的initialize方法。