先來(lái)看一個(gè)升級(jí)版面試題:
1、load與initialize分別是何時(shí)調(diào)用的?以及l(fā)oad與initialize這兩個(gè)方法的在父類(lèi),子類(lèi),分類(lèi)之間的調(diào)用順序是怎樣的?
2、分類(lèi)實(shí)現(xiàn)了類(lèi)的initialize方法,那么類(lèi)的方法initialize還會(huì)調(diào)用嗎?為什么?
針對(duì)這個(gè)面試題,我們繼續(xù)深入底層,本篇文章結(jié)構(gòu):
- load函數(shù)與initialize函數(shù)調(diào)用時(shí)機(jī)
- 類(lèi)(Class)的方法和分類(lèi)(Category)的方法之間的調(diào)用關(guān)系(為什么分類(lèi)的方法會(huì)覆蓋類(lèi)的方法)
- 面試題答案(筆者總結(jié),僅供參考)
一、load函數(shù)與initialize函數(shù)調(diào)用時(shí)機(jī)及順序
新建工程,實(shí)現(xiàn)父類(lèi)BMPerson、子類(lèi)BMStudent和子類(lèi)的分類(lèi)BMStudent(Cover),分別重寫(xiě)這三個(gè)類(lèi)的load以及initialize,在main函數(shù)里面也做個(gè)函數(shù)打印,運(yùn)行后打印結(jié)果如下
從打印結(jié)果我們粗略的能看出:
不管是子類(lèi),父類(lèi)還是分類(lèi),load方法的調(diào)用都在main函數(shù)之前就已經(jīng)調(diào)用了
而initialize方法則是在main函數(shù)之后,也就是程序運(yùn)行的時(shí)候才開(kāi)始調(diào)用
先來(lái)看load,結(jié)合筆者上篇深入App啟動(dòng)之dyld、map_images、load_images,我們其實(shí)知道:
load方法調(diào)用時(shí)機(jī)其實(shí)就是在程序運(yùn)行,Runtime進(jìn)行
load_images
時(shí)調(diào)用的,在main函數(shù)之前,父類(lèi)子類(lèi)分類(lèi)的調(diào)用順序是:先調(diào)用類(lèi),后調(diào)用所有分類(lèi);調(diào)用類(lèi)會(huì)先遞歸調(diào)用父類(lèi),后調(diào)用子類(lèi);分類(lèi)和類(lèi)的調(diào)用順序沒(méi)有關(guān)系,是根據(jù)Mach-O文件的順序進(jìn)行調(diào)用的。
接下里我們分析initialize的調(diào)用時(shí)機(jī)及調(diào)用關(guān)系。
由于我們同時(shí)打印父類(lèi),子類(lèi),分類(lèi)發(fā)現(xiàn)子類(lèi)的并不調(diào)用,接下來(lái)我們注釋掉分類(lèi)的initialize,查看打印結(jié)果:
然后在子類(lèi)的initialize中打上斷點(diǎn),查看函數(shù)調(diào)用堆棧:
利用控制變量的思想,從以上的所有打印結(jié)果,我們能得出:
1、子類(lèi)父類(lèi)分類(lèi)的調(diào)用順序是:如果實(shí)現(xiàn)了分類(lèi):先父類(lèi)后分類(lèi),并且不再調(diào)用原來(lái)子類(lèi)中的initialize;如果沒(méi)有實(shí)現(xiàn)分類(lèi):先父類(lèi)后子類(lèi)
2、initialize方法調(diào)用時(shí)機(jī)是在Class對(duì)象進(jìn)行初始化時(shí),通過(guò)Runtime的消息轉(zhuǎn)發(fā)機(jī)制,查找方法的imp然后進(jìn)行調(diào)用的,對(duì)比load方法,它是在main函數(shù)之后,對(duì)象創(chuàng)建初始化的時(shí)候調(diào)用的。
那么問(wèn)題來(lái)了:為什么分類(lèi)的initialize會(huì)覆蓋類(lèi)的initialize呢?接下來(lái)我們從源碼進(jìn)行分析
二、類(lèi)(Class)的方法和分類(lèi)(Category)的方法之間的調(diào)用關(guān)系(為什么分類(lèi)的方法會(huì)覆蓋類(lèi)的方法)
先思考:為什么分類(lèi)的方法會(huì)覆蓋類(lèi)的方法呢?我們知道方法調(diào)用底層就是通過(guò)Runtime進(jìn)行消息轉(zhuǎn)發(fā),去對(duì)應(yīng)類(lèi)的methodList進(jìn)行方法編號(hào)imp查找,然后調(diào)用 而且上一篇深入App啟動(dòng)之dyld、map_images、load_images對(duì)map_images
進(jìn)行分析過(guò),在類(lèi)的結(jié)構(gòu)中方法都存儲(chǔ)在data的methods方法表里面,這個(gè)表的類(lèi)型是method_list_t
,method_list_t
的父類(lèi)list_array_tt
會(huì)提供attachLists
方法把分類(lèi)的方法都添加到類(lèi)里面,中間也沒(méi)有進(jìn)行任何去重這種敏感的操作,而且從Mach-O文件中我們也能看出:類(lèi)的方法并沒(méi)有被分類(lèi)覆蓋掉,這類(lèi)的initialize方法以及分類(lèi)的initialize方法的地址也不一樣,這兩個(gè)方法都還存在。
既然存的時(shí)候,都存進(jìn)去了,那么只有一種可能:在方法調(diào)用的時(shí)候,肯定做了只會(huì)讀分類(lèi)的方法的邏輯操作!
從上面斷點(diǎn)打印的調(diào)用堆棧信息,我們直接進(jìn)入Objc
源碼搜索lookUpImpOrForward
,代碼如下
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//1、先從緩存查找,如果有就取出來(lái)cache_getImp;緩存沒(méi)有,先看類(lèi)是否實(shí)現(xiàn),如果沒(méi)實(shí)現(xiàn)就去實(shí)現(xiàn)并初始化
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
......
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
//2.開(kāi)始retry查找
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//從這個(gè)類(lèi)的緩存中查找
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
//
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
//從父類(lèi)的緩存以及方法列表里面進(jìn)行查找
{
......
}
// No implementation found. Try method resolver once.
//沒(méi)有找到,嘗試一次動(dòng)態(tài)方法解析_class_resolveMethod,方法還是找不到imp,看看開(kāi)發(fā)者是否實(shí)現(xiàn)預(yù)留的方法resolveInstanceMethod或者resolveClassMethod
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
......
triedResolver = YES;
goto retry;
}
//還是找不到,就進(jìn)行消息轉(zhuǎn)發(fā),打印方法找不到
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
整個(gè)方法lookUpImpOrForward的imp查找過(guò)程大致就是三步:
- 從Optimistic cache緩存中查找
- 找不到先判斷類(lèi)是否實(shí)現(xiàn),如果未實(shí)現(xiàn)就進(jìn)行實(shí)現(xiàn)
- 然后開(kāi)始retry查找
retry中的imp查找過(guò)程就是
- 先查找類(lèi)的緩存和方法列表
- 在查找父類(lèi)的緩存和方法列表
- 以上都找不到就進(jìn)行一次動(dòng)態(tài)方法解析,查看開(kāi)發(fā)者針對(duì)該類(lèi)有沒(méi)有實(shí)現(xiàn)了設(shè)計(jì)時(shí)預(yù)留的方法resolveInstanceMethod或者resolveClassMethod
- 如果動(dòng)態(tài)方法解析還找不到就進(jìn)行消息轉(zhuǎn)發(fā),然后打印方法找不到
我們的場(chǎng)景主要是查看initialize方法的調(diào)用順序,所以查看第一步,從類(lèi)里面找就行了。
找到類(lèi)方法查找的關(guān)鍵函數(shù)getMethodNoSuper_nolock
并找到關(guān)鍵函數(shù)search_method_list
點(diǎn)擊進(jìn)入,下面貼上這兩個(gè)函數(shù)的源碼
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
在search_method_list
找到關(guān)鍵函數(shù)findMethodInSortedMethodList
,重點(diǎn)來(lái)了!?。。。。。。。?!
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
注意其中for循環(huán)中的一段核心代碼及注釋?zhuān)∵@段代碼正是category覆蓋類(lèi)方法的關(guān)鍵點(diǎn)!這段代碼的邏輯就是:**倒序查找方法的第一次實(shí)現(xiàn) **
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
結(jié)合之前我們分析map_images
加載順序:先加載父類(lèi)->再子類(lèi)->所有類(lèi)的分類(lèi)。所以在消息轉(zhuǎn)發(fā)查找imp的時(shí)候,一定會(huì)從表的后邊往前邊查,而分類(lèi)中的方法正是最后添加的!所以如果這個(gè)類(lèi)分類(lèi)也實(shí)現(xiàn)了這個(gè)方法,一定會(huì)先找分類(lèi)中的方法,這里的邏輯正是分類(lèi)重寫(xiě)的精髓所在!
知道了為啥分類(lèi)中的方法會(huì)覆蓋類(lèi)中的方法之后,筆者從源碼中也看出了分類(lèi)方法會(huì)覆蓋類(lèi)中的,但是分類(lèi)之間是沒(méi)有絕對(duì)的先后順序的,所以我們?cè)跒轭?lèi)添加分類(lèi)的時(shí)候需要注意這一點(diǎn),不然可能會(huì)導(dǎo)致分類(lèi)之間互相影響。
三、面試題答案(筆者總結(jié),僅供參考)
1、load與initialize分別是何時(shí)初始化的?以及l(fā)oad與initialize這兩個(gè)方法的在父類(lèi),子類(lèi),分類(lèi)之間的調(diào)用順序是怎樣的?
load調(diào)用時(shí)機(jī)
main函數(shù)之前,Runtime進(jìn)行
load_images
時(shí)調(diào)用
load調(diào)用順序
父類(lèi)子類(lèi)分類(lèi)的調(diào)用順序是:先調(diào)用類(lèi),后調(diào)用所有分類(lèi);調(diào)用類(lèi)會(huì)先遞歸調(diào)用父類(lèi),后調(diào)用子類(lèi);分類(lèi)和類(lèi)的調(diào)用順序沒(méi)有關(guān)系,是根據(jù)Mach-O文件的順序進(jìn)行調(diào)用的。
initialize調(diào)用時(shí)機(jī)
main函數(shù)之后,Runtime通過(guò)消息轉(zhuǎn)發(fā)查找方法的imp,在
lookUpImpOrForward
時(shí),在類(lèi)的方法列表中找到并調(diào)用
initialize調(diào)用順序
如果分類(lèi)中重寫(xiě)了initialize方法,則調(diào)用順序:先父類(lèi)后分類(lèi)
如果分類(lèi)未重寫(xiě)initialize方法,則調(diào)用順序:先父類(lèi)后子類(lèi)
2、分類(lèi)實(shí)現(xiàn)了類(lèi)的initialize方法,那么類(lèi)的方法initialize還會(huì)調(diào)用嗎?為什么?
分類(lèi)中實(shí)現(xiàn)的類(lèi)的initialize方法,那么類(lèi)的方法就不會(huì)調(diào)用了。
之所以出現(xiàn)這種覆蓋的假象,是因?yàn)?code>map_images操作方法的時(shí)候,是先處理類(lèi)后處理分類(lèi)的,所以方法存進(jìn)類(lèi)的方法的順序是:先添加類(lèi),后添加分類(lèi)。但是在Runtime查找imp的時(shí)候,是倒序查找類(lèi)的方法列表中第一個(gè)出現(xiàn)的方法,只要找到第一個(gè)就直接返回了,所以會(huì)出現(xiàn)分類(lèi)方法覆蓋類(lèi)方法的假象。