iOS進(jìn)階專(zhuān)項(xiàng)分析(九)、load與initialize,類(lèi)與分類(lèi)的方法之間的關(guān)系

先來(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):

  1. load函數(shù)與initialize函數(shù)調(diào)用時(shí)機(jī)
  2. 類(lèi)(Class)的方法和分類(lèi)(Category)的方法之間的調(diào)用關(guān)系(為什么分類(lèi)的方法會(huì)覆蓋類(lèi)的方法)
  3. 面試題答案(筆者總結(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é)果如下

load及initialize調(diào)用時(shí)機(jī)順序.png

從打印結(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.png

然后在子類(lèi)的initialize中打上斷點(diǎn),查看函數(shù)調(diào)用堆棧:

initialize子類(lèi)調(diào)用堆棧.png

利用控制變量的思想,從以上的所有打印結(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ǔ)在datamethods方法表里面,這個(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è)方法都還存在。

Mach-O文件查看方法地址.png

既然存的時(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ò)程大致就是三步:

  1. 從Optimistic cache緩存中查找
  2. 找不到先判斷類(lèi)是否實(shí)現(xiàn),如果未實(shí)現(xiàn)就進(jìn)行實(shí)現(xiàn)
  3. 然后開(kāi)始retry查找

retry中的imp查找過(guò)程就是

  1. 先查找類(lèi)的緩存和方法列表
  2. 在查找父類(lèi)的緩存和方法列表
  3. 以上都找不到就進(jìn)行一次動(dòng)態(tài)方法解析,查看開(kāi)發(fā)者針對(duì)該類(lèi)有沒(méi)有實(shí)現(xiàn)了設(shè)計(jì)時(shí)預(yù)留的方法resolveInstanceMethod或者resolveClassMethod
  4. 如果動(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)方法的假象。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。