Category的本質(zhì)<一>
Category的本質(zhì)<三>關(guān)聯(lián)對象
面試題1:Category中有l(wèi)oad方法嗎辱志?load方法是什么時候調(diào)用?
面試題2:load狞膘,initialize的區(qū)別是什么揩懒?它們在Category中的調(diào)用順序以及出現(xiàn)繼承時它們之間的調(diào)用過程是怎么樣的?
那么這篇文章主要就是回答這兩個問題挽封。
load方法
load方法什么時候調(diào)用已球?
load方法是在runtime加載類和分類的時候調(diào)用。
我們創(chuàng)建了一個Person類和它的兩個分類场仲,然后重寫了各自的load方法:
//Person
+ (void)load{
NSLog(@"Person + load");
}
//Person+Test1
+ (void)load{
NSLog(@"Person (Test1) + load");
}
//Person+Test2
+ (void)load{
NSLog(@"Person (Test2) + load");
}
然后我們什么也不做和悦,運行代碼,看到打印結(jié)果:
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
通過打印結(jié)果我們可以看到Person及其分類的load方法都被調(diào)用了渠缕,這就證實了load方法是由runtime加載類和分類的時候調(diào)用的鸽素。
然后我們再給Person類及其子類創(chuàng)建一個+ (void)test
方法并實現(xiàn)它:
//Person
+ (void)test{
NSLog(@"Person + test");
}
//Person+Test1
+ (void)test{
NSLog(@"Person (Test1) + test");
}
//Person+Test2
+ (void)test{
NSLog(@"Person (Test2) + test");
}
然后用Person類對象去調(diào)用test方法:
[Person test];
得到打印結(jié)果:
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
通過打印結(jié)果我們可以看到,Person (Test2)的test方法被調(diào)用了亦鳞,這個很好理解因為我們在Category的本質(zhì)<一>中說的很清楚了馍忽,如果分類和類同時實現(xiàn)了一個方法棒坏,那么分類中的方法和類中的方法都會保存下來存入內(nèi)存中,并且分類的方法在前遭笋,類的方法在后坝冕,這樣在調(diào)用的時候就會首先找到分類的方法,給人的感覺就是好像類的方法被覆蓋了瓦呼。
那么問題來了喂窟,同樣是類方法,同樣是分類中實現(xiàn)了類的方法央串,為什么load方法不像test方法一樣磨澡,調(diào)用分類的實現(xiàn),而是類和每個分類中的load方法都被調(diào)用了呢质和?load方法到底有什么不同呢稳摄?
要想弄清楚其中的原理,我們還是要從runtime的源碼入手:
- 1.找到objc-os.mm這個文件饲宿,然后找到這個文件的
void _objc_init(void)
這個方法厦酬,runtime的初始化都是在這個方法里面完成。 - 2.這個方法的最后一行調(diào)用了函數(shù)
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
,我們點進load_images瘫想,這是加載模塊的意思仗阅。
-
3.
-
4
- 5我們點進
call_class_loads();
這個方法查看對類的load方法的調(diào)用過程:
- 6.然后我們再點進
call_category_loads()
查看對分類的load方法的調(diào)用過程:
那么這樣我們就搞清楚了為什么load方法不是像test方法一樣,執(zhí)行分類的實現(xiàn)
因為load方法的調(diào)用并不是objc_msgSend機制殿托,它是直接找到類的load方法的地址霹菊,然后調(diào)用類的load方法,然后再找到分類的load方法的地址支竹,再去調(diào)用它。
而test方法是通過消息機制去調(diào)用的鸠按。首先找到類對象礼搁,由于test方法是類方法,存儲在元類對象中目尖,所以通過類對象的isa指針找到元類對象馒吴,然后在元類對象中尋找test方法,由于分類也實現(xiàn)了test方法瑟曲,所以分類的test方法是在類的test方法的前面饮戳,首先找到了分類的test方法,然后去調(diào)用它洞拨。
有繼承關(guān)系時load方法的調(diào)用順序
通過上面的分析我們確定了load方法的一個調(diào)用規(guī)則:先調(diào)用所有類的load方法扯罐,然后再調(diào)用所有分類的load方法。
下面我們再創(chuàng)建一個Student類繼承自Person類烦衣,并且為Student類創(chuàng)建兩個子類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");
}
然后我們運行一下程序歹河,看打印結(jié)果:
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
通過打印結(jié)果我們可以很清楚的看見掩浙,Person類和Student類的load方法先被調(diào)用,然后調(diào)用分類的load方法秸歧。再運行多次厨姚,都是Person類和Student類的load方法先被調(diào)用,然后分類的方法才被調(diào)用键菱。并且總是Person類的load在Student類的load方法前面被調(diào)用谬墙,這會不會和編譯順序有關(guān)呢?我們改變一下編譯順序看看:
TARGETS -> Build Phases -> Complle Sources中文件的放置順序就是文件的編譯順序经备。
目前是Person類在Student類的前面編譯拭抬,現(xiàn)在我們把Student類放到Person類的前面編譯:
然后我們再運行一下程序,查看打印結(jié)果:
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
我們發(fā)現(xiàn)還是Person類的load方法在Student類前面被調(diào)用弄喘,所以好像和編譯順序無關(guān)呀玖喘。那么我們就需要思考一下是不是由于Student和Person之間的繼承關(guān)系導致的呢?
為了搞清楚這個問題蘑志,我們只能從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();
這個方法戒努,上面已經(jīng)講到,這是調(diào)用類的load方法镐躲。點進去储玫。 -
4
- 5.為了搞清楚這里的classes數(shù)組的來歷,我們回退到
void load_images(const char *path __unused, const struct mach_header *mh)
這個方法萤皂,這個方法中有一個prepare_load_methods((const headerType *)mh);
這個方法撒穷,根據(jù)方法名可能和我們的問題有關(guān)。因此我們點進這個方法查看一下 -
6.
- 7.點進
schedule_class_load(remapClass(classlist[i]));
這個方法:通過這個方法我們就可以很清晰的看到裆熙,當要把一個類加入最終的這個classes數(shù)組的時候端礼,會先去上溯這個類的父類,先把父類加入這個數(shù)組入录。
由于在classes數(shù)組中父類永遠在子類的前面蛤奥,所以在加載類的load方法時一定是先加載父類的load方法,再加載子類的load方法僚稿。
類的load方法調(diào)用順序搞清楚了我們再來看一下分類的load方法調(diào)用順序
我們還是看一下void prepare_load_methods(const headerType *mhdr)
這個函數(shù)
下面我們通過打印結(jié)果驗證一下望蜡,這是編譯順序:通過這個分析我們就能知道凡桥,分類的load方法加載順序很簡單,就是誰先編譯的贫奠,誰的load方法就被先加載唬血。
按照我們前面的分析,load方法的調(diào)用順序應該是:
Person -> Student -> Person + Test1 -> Student + Test2 -> Student + Test1 -> Person + Test2拷恨。
我們看一下打印結(jié)果:
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
打印結(jié)果完美的驗證了我們的結(jié)論脖律。
總結(jié) load方法調(diào)用順序
1.先調(diào)用類的load方法
- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
- 調(diào)用子類的load方法之前會先調(diào)用父類的load方法
2.再調(diào)用分類的load方法
- 按照編譯先后順序腕侄,先編譯小泉,先調(diào)用
initialize方法
initialize方法的調(diào)用時機
- initialize在類第一次接收到消息時調(diào)用,也就是objc_msgSend()冕杠。
- 先調(diào)用父類的+initialize微姊,再調(diào)用子類的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");
}
我們運行程序分预,發(fā)現(xiàn)什么也沒有打印兢交,說明在運行期沒有調(diào)用+initialize方法。
然后我們給Person類發(fā)送消息笼痹,也就是調(diào)用函數(shù):
[Person alloc];
打印結(jié)果:
2018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize
可以看到調(diào)用了Person類的分類的initialize方法配喳。通過這個打印結(jié)果我們能看出initialize方法和load方法的不同,load方法由于是直接獲取方法的地址凳干,然后調(diào)用方法晴裹,所以Person及其分類的load方法都會調(diào)用。而initialize方法則更像是通過消息機制救赐,也即是objc_msgend(Person, @selector(initialize))這種來調(diào)用的涧团。
然后我多次調(diào)用alloc方法:
[Person alloc];
[Person alloc];
[Person alloc];
打印結(jié)果:
018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize
可見initialize方法只在類第一次收到消息時調(diào)用。然后我們再給Student類發(fā)送消息:
[Student alloc];
打印結(jié)果:
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
我們看到不僅調(diào)用了Student類的initialize方法经磅,而且還調(diào)用了Student類的父類泌绣,Person類的方法,因此我們猜測在調(diào)用類的initialize方法之前會先調(diào)用父類的initialize方法预厌。
以上僅僅是我們根據(jù)打印結(jié)果的猜測赞别,還需要通過源碼來驗證。
[Person alloc]
就相當于objc_msgSend([Person class], @selector(alloc))
,說明objc_msgSend()內(nèi)部會去調(diào)用initialize方法配乓,判斷是第幾次接收到消息。
- 1.我們?nèi)untime源碼中搜索
class_getClassmethod
方法惠毁,會在objc-class.mm這個文件中找到這個方法的實現(xiàn): - 2.我們點進
class_getInstanceMethod(cls->getMeta(), sel);
這個方法:
-
3.點進這個方法:
- 4.繼續(xù)尋找
lookUpImpOrForward
這個方法的實現(xiàn)犹芹,我截取其中有價值的代碼塊: - 5.我們點進
_class_initialize (_class_getNonMetaClass(cls, inst));
尋找真正的實現(xiàn): - 6.然后我們通過
callInitialize(cls);
查看具體的調(diào)
這樣一來+initialize方法的調(diào)用過程就很清楚了鞠绰。
+initialize的調(diào)用過程:
- 1查看本類的initialize方法有沒有實現(xiàn)過腰埂,如果已經(jīng)實現(xiàn)過就返回,不再實現(xiàn)蜈膨。
- 2.如果本類沒有實現(xiàn)過initialize方法屿笼,那么就去遞歸查看該類的父類有沒有實現(xiàn)過initialize方法牺荠,如果沒有實現(xiàn)就去實現(xiàn),最后實現(xiàn)本類的initialize方法驴一。并且initialize方法是通過objc_msgSend()實現(xiàn)的休雌。
+initialize和+load的一個很大區(qū)別是,+initialize是通過objc_msgSend進行調(diào)用的肝断,所以有以下特點:
- 如果子類沒有實現(xiàn)+initialize方法杈曲,會調(diào)用父類的+initialize(所以父類的+initialize方法可能會被調(diào)用多次)
- 如果分類實現(xiàn)了+initialize,會覆蓋類本身的+initialize調(diào)用胸懈。
下面我們把Student類及其分類中的+initialize這個方法的實現(xiàn)去掉担扑,然后增加一個Teacher類繼承自Person類。然后我們給Student類和Teacher類都發(fā)送alloc消息:
[Student alloc];
[Teacher alloc];
這個時候也就是只有Person類及其分類實現(xiàn)了+initialize方法趣钱。那么打印結(jié)果會是怎樣呢涌献?
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方法竟然被調(diào)用了三次,這多少有些出乎意外吧首有。下面我們來分析一下燕垃。
BOOL studentInitialized = NO;
BOOL personinitialized = NO;
BOOL teacherInitialized = NO;
[Student alloc];
//判斷Student類是否初始化了,這里Student類還沒有被初始化绞灼,所以進入條件語句利术。
if(!studentInitialized){
//判斷Student類的父類Person類是否初始化了
if(!personinitialized){
//這里Person類還沒有初始化,就利用objc_msgSend調(diào)用initialize方法
objc_msgSend([Person class], @selector(initialize));
//變更Person類是否初始化的狀態(tài)
personinitialized = YES;
}
//利用objc_msgSend調(diào)用Student的initialize方法
objc_msgSend([Student class], @selector(initialize));
//變更Student是否初始化的狀態(tài)
studentInitialized = YES
}
[Teacher alloc];
//判斷Teacher類是否已經(jīng)初始化了低矮,這里Teacher類還沒有初始化印叁,進入條件語句
if(!teacherInitialized){
//判斷其父類Person類是否初始化了,這里父類已經(jīng)初始化了军掂,所以不會進入這個條件語句
if(!personinitialized){
objc_msgSend([Person class], @selector(initialize));
personinitialized = YES;
}
//利用objc_msgSend調(diào)用Teacher類的initialize方法
objc_msgSend([Teacher class], @selector(initialize));
//變更狀態(tài)
teacherInitialized = YES;
}
上面列出來的是調(diào)用initialize的偽代碼轮蜕,下面再詳細說明這個過程:
- 1.Student類收到alloc消息,開始著手準備調(diào)用initialize方法蝗锥。首先判斷自己有沒有初始化過跃洛。
- 2.判斷自己沒有初始化過,所以就去找自己的父類Person類终议,看Person類有沒有初始化過汇竭,發(fā)現(xiàn)Person類也沒有初始化過,且Person類也沒有父類穴张,多以對Person類使用
objc_msgSend([Person class], @selector(initialize))
調(diào)用Person類的initialize方法细燎。這是第一次調(diào)用Person類的initialize方法。 - 3.父類處理完后皂甘,再通過
objc_msgSend([Student class], @selector(initialize));
調(diào)用Student類的initialize方法玻驻,但是由于Student類沒有實現(xiàn)initialize方法,所以通過其superclass指針找到父類Person類偿枕,然后調(diào)用了Person類的initialize實現(xiàn)璧瞬。這是第二次調(diào)用Person類的initialize方法户辫。 - 4.Teacher類收到alloc方法,開始準備調(diào)用initialize放啊發(fā)嗤锉。首先判斷自己有沒有被初始化過渔欢。
- 5.判斷自己沒有被初始化過后,又開始判斷其父類Person類有沒有被初始化過档冬,剛剛父類Person類已經(jīng)被初始化過膘茎。
- 6.于是通過
objc_msgSend([Teacher class], @selector(initialize))
調(diào)用Teacher類的initialize方法。但是由于Teacher類沒有實現(xiàn)initialize方法酷誓,所以只能通過superclass指針去查找父類有沒有實現(xiàn)initialize方法披坏,發(fā)現(xiàn)父類Person類實現(xiàn)了initialize方法,于是調(diào)用父類的initialize方法盐数。這是第三次調(diào)用Person類的initialize方法棒拂。