iOS進階專項分析(九)、load與initialize,類與分類的方法之間的關系

先來看一個升級版面試題:

1、load與initialize分別是何時調用的?以及load與initialize這兩個方法的在父類,子類,分類之間的調用順序是怎樣的?
2、分類實現了類的initialize方法,那么類的方法initialize還會調用嗎?為什么?

針對這個面試題,我們繼續深入底層,本篇文章結構:

  1. load函數與initialize函數調用時機
  2. 類(Class)的方法和分類(Category)的方法之間的調用關系(為什么分類的方法會覆蓋類的方法)
  3. 面試題答案(筆者總結,僅供參考)

一、load函數與initialize函數調用時機及順序


新建工程,實現父類BMPerson、子類BMStudent和子類的分類BMStudent(Cover),分別重寫這三個類的load以及initialize,在main函數里面也做個函數打印,運行后打印結果如下

load及initialize調用時機順序.png

從打印結果我們粗略的能看出:

不管是子類,父類還是分類,load方法的調用都在main函數之前就已經調用了

而initialize方法則是在main函數之后,也就是程序運行的時候才開始調用

先來看load,結合筆者上篇深入App啟動之dyld、map_images、load_images,我們其實知道:

load方法調用時機其實就是在程序運行,Runtime進行load_images時調用的,在main函數之前,父類子類分類的調用順序是:先調用類,后調用所有分類;調用類會先遞歸調用父類,后調用子類;分類和類的調用順序沒有關系,是根據Mach-O文件的順序進行調用的。

接下里我們分析initialize的調用時機及調用關系。

由于我們同時打印父類,子類,分類發現子類的并不調用,接下來我們注釋掉分類的initialize,查看打印結果:

注釋掉分類的initialize.png

然后在子類的initialize中打上斷點,查看函數調用堆棧:

initialize子類調用堆棧.png

利用控制變量的思想,從以上的所有打印結果,我們能得出:

1、子類父類分類的調用順序是:如果實現了分類:先父類后分類,并且不再調用原來子類中的initialize;如果沒有實現分類:先父類后子類

2、initialize方法調用時機是在Class對象進行初始化時,通過Runtime的消息轉發機制,查找方法的imp然后進行調用的,對比load方法,它是在main函數之后,對象創建初始化的時候調用的。

那么問題來了:為什么分類的initialize會覆蓋類的initialize呢?接下來我們從源碼進行分析

二、類(Class)的方法和分類(Category)的方法之間的調用關系(為什么分類的方法會覆蓋類的方法)


先思考:為什么分類的方法會覆蓋類的方法呢?我們知道方法調用底層就是通過Runtime進行消息轉發,去對應類的methodList進行方法編號imp查找,然后調用 而且上一篇深入App啟動之dyld、map_images、load_imagesmap_images進行分析過,在類的結構中方法都存儲在datamethods方法表里面,這個表的類型是method_list_tmethod_list_t的父類list_array_tt會提供attachLists方法把分類的方法都添加到類里面,中間也沒有進行任何去重這種敏感的操作,而且從Mach-O文件中我們也能看出:類的方法并沒有被分類覆蓋掉,這類的initialize方法以及分類的initialize方法的地址也不一樣,這兩個方法都還存在。

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

既然存的時候,都存進去了,那么只有一種可能:在方法調用的時候,肯定做了只會讀分類的方法的邏輯操作!

從上面斷點打印的調用堆棧信息,我們直接進入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查找過程大致就是三步:

  1. 從Optimistic cache緩存中查找
  2. 找不到先判斷類是否實現,如果未實現就進行實現
  3. 然后開始retry查找

retry中的imp查找過程就是

  1. 先查找類的緩存和方法列表
  2. 在查找父類的緩存和方法列表
  3. 以上都找不到就進行一次動態方法解析,查看開發者針對該類有沒有實現了設計時預留的方法resolveInstanceMethod或者resolveClassMethod
  4. 如果動態方法解析還找不到就進行消息轉發,然后打印方法找不到

我們的場景主要是查看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的時候,是倒序查找類的方法列表中第一個出現的方法,只要找到第一個就直接返回了,所以會出現分類方法覆蓋類方法的假象。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。