先來看一個升級版面試題:
1、load與initialize分別是何時調用的?以及load與initialize這兩個方法的在父類,子類,分類之間的調用順序是怎樣的?
2、分類實現了類的initialize方法,那么類的方法initialize還會調用嗎?為什么?
針對這個面試題,我們繼續深入底層,本篇文章結構:
- load函數與initialize函數調用時機
- 類(Class)的方法和分類(Category)的方法之間的調用關系(為什么分類的方法會覆蓋類的方法)
- 面試題答案(筆者總結,僅供參考)
一、load函數與initialize函數調用時機及順序
新建工程,實現父類BMPerson、子類BMStudent和子類的分類BMStudent(Cover),分別重寫這三個類的load以及initialize,在main函數里面也做個函數打印,運行后打印結果如下
從打印結果我們粗略的能看出:
不管是子類,父類還是分類,load方法的調用都在main函數之前就已經調用了
而initialize方法則是在main函數之后,也就是程序運行的時候才開始調用
先來看load,結合筆者上篇深入App啟動之dyld、map_images、load_images,我們其實知道:
load方法調用時機其實就是在程序運行,Runtime進行
load_images
時調用的,在main函數之前,父類子類分類的調用順序是:先調用類,后調用所有分類;調用類會先遞歸調用父類,后調用子類;分類和類的調用順序沒有關系,是根據Mach-O文件的順序進行調用的。
接下里我們分析initialize的調用時機及調用關系。
由于我們同時打印父類,子類,分類發現子類的并不調用,接下來我們注釋掉分類的initialize,查看打印結果:
然后在子類的initialize中打上斷點,查看函數調用堆棧:
利用控制變量的思想,從以上的所有打印結果,我們能得出:
1、子類父類分類的調用順序是:如果實現了分類:先父類后分類,并且不再調用原來子類中的initialize;如果沒有實現分類:先父類后子類
2、initialize方法調用時機是在Class對象進行初始化時,通過Runtime的消息轉發機制,查找方法的imp然后進行調用的,對比load方法,它是在main函數之后,對象創建初始化的時候調用的。
那么問題來了:為什么分類的initialize會覆蓋類的initialize呢?接下來我們從源碼進行分析
二、類(Class)的方法和分類(Category)的方法之間的調用關系(為什么分類的方法會覆蓋類的方法)
先思考:為什么分類的方法會覆蓋類的方法呢?我們知道方法調用底層就是通過Runtime進行消息轉發,去對應類的methodList進行方法編號imp查找,然后調用 而且上一篇深入App啟動之dyld、map_images、load_images對map_images
進行分析過,在類的結構中方法都存儲在data的methods方法表里面,這個表的類型是method_list_t
,method_list_t
的父類list_array_tt
會提供attachLists
方法把分類的方法都添加到類里面,中間也沒有進行任何去重這種敏感的操作,而且從Mach-O文件中我們也能看出:類的方法并沒有被分類覆蓋掉,這類的initialize方法以及分類的initialize方法的地址也不一樣,這兩個方法都還存在。
既然存的時候,都存進去了,那么只有一種可能:在方法調用的時候,肯定做了只會讀分類的方法的邏輯操作!
從上面斷點打印的調用堆棧信息,我們直接進入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、先從緩存查找,如果有就取出來cache_getImp;緩存沒有,先看類是否實現,如果沒實現就去實現并初始化
// 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.開始retry查找
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//從這個類的緩存中查找
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.
//從父類的緩存以及方法列表里面進行查找
{
......
}
// No implementation found. Try method resolver once.
//沒有找到,嘗試一次動態方法解析_class_resolveMethod,方法還是找不到imp,看看開發者是否實現預留的方法resolveInstanceMethod或者resolveClassMethod
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
......
triedResolver = YES;
goto retry;
}
//還是找不到,就進行消息轉發,打印方法找不到
// 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;
}
整個方法lookUpImpOrForward的imp查找過程大致就是三步:
- 從Optimistic cache緩存中查找
- 找不到先判斷類是否實現,如果未實現就進行實現
- 然后開始retry查找
retry中的imp查找過程就是
- 先查找類的緩存和方法列表
- 在查找父類的緩存和方法列表
- 以上都找不到就進行一次動態方法解析,查看開發者針對該類有沒有實現了設計時預留的方法resolveInstanceMethod或者resolveClassMethod
- 如果動態方法解析還找不到就進行消息轉發,然后打印方法找不到
我們的場景主要是查看initialize方法的調用順序,所以查看第一步,從類里面找就行了。
找到類方法查找的關鍵函數getMethodNoSuper_nolock
并找到關鍵函數search_method_list
點擊進入,下面貼上這兩個函數的源碼
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
找到關鍵函數findMethodInSortedMethodList
,重點來了!!!!!!!!!!
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循環中的一段核心代碼及注釋!這段代碼正是category覆蓋類方法的關鍵點!這段代碼的邏輯就是:**倒序查找方法的第一次實現 **
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;
}
結合之前我們分析map_images
加載順序:先加載父類->再子類->所有類的分類。所以在消息轉發查找imp的時候,一定會從表的后邊往前邊查,而分類中的方法正是最后添加的!所以如果這個類分類也實現了這個方法,一定會先找分類中的方法,這里的邏輯正是分類重寫的精髓所在!
知道了為啥分類中的方法會覆蓋類中的方法之后,筆者從源碼中也看出了分類方法會覆蓋類中的,但是分類之間是沒有絕對的先后順序的,所以我們在為類添加分類的時候需要注意這一點,不然可能會導致分類之間互相影響。
三、面試題答案(筆者總結,僅供參考)
1、load與initialize分別是何時初始化的?以及load與initialize這兩個方法的在父類,子類,分類之間的調用順序是怎樣的?
load調用時機
main函數之前,Runtime進行
load_images
時調用
load調用順序
父類子類分類的調用順序是:先調用類,后調用所有分類;調用類會先遞歸調用父類,后調用子類;分類和類的調用順序沒有關系,是根據Mach-O文件的順序進行調用的。
initialize調用時機
main函數之后,Runtime通過消息轉發查找方法的imp,在
lookUpImpOrForward
時,在類的方法列表中找到并調用
initialize調用順序
如果分類中重寫了initialize方法,則調用順序:先父類后分類
如果分類未重寫initialize方法,則調用順序:先父類后子類
2、分類實現了類的initialize方法,那么類的方法initialize還會調用嗎?為什么?
分類中實現的類的initialize方法,那么類的方法就不會調用了。
之所以出現這種覆蓋的假象,是因為
map_images
操作方法的時候,是先處理類后處理分類的,所以方法存進類的方法的順序是:先添加類,后添加分類。但是在Runtime查找imp的時候,是倒序查找類的方法列表中第一個出現的方法,只要找到第一個就直接返回了,所以會出現分類方法覆蓋類方法的假象。