Category詳解(二)—— 源碼層面解析load和initialize方法

寫在前面

+load 和 +initialize 方法你們確定不陌生,咱們的項目中會有不少重寫這兩個方法的地方,可是你有沒有想過他們有什麼區別?產生區別的緣由是什麼?今天咱們就從源碼的層面來解答一下這些問題。數組

閱讀文章時請注意:留意源碼中我增長註釋的部分,每每是該函數的重點部分。markdown

問題

針對 load 和 initialize 方法,咱們先來思考幾個問題:app

  1. 分類中有 load 和 initialize 方法嗎?
  2. 若是有,那麼分類會覆蓋原類的方法嗎?
  3. 調用順序是怎樣的,子類和父類哪一個先?
  4. 調用子類時會不會調用父類的方法?

因爲源碼較多,將所有源碼貼出來不太現實,所以最好的方法是下載runtime源碼,跟着文章一塊兒閱讀。函數

+load方法

調用時機

load方法的調用時機是runtime加載類和分類的時候調用。
咱們跟上一篇文章同樣,定義一個Person類和Person的兩個分類ui

// Person
+ (void)load {
    NSLog(@"person");
}
+ (void)test {
    NSLog(@"person test");
}
 
// Person + Eat
+ (void)load {
    NSLog(@"Person Eat");
}
+(void)test {
    NSLog(@"Person(Eat) test");
}
 
2020-06-01 19:25:49.718843+0800 TestCategory[38073:13350285] person
2020-06-01 19:25:49.719425+0800 TestCategory[38073:13350285] Person Eat
2020-06-01 19:25:49.895610+0800 TestCategory[38073:13350285] Person(Eat) test
複製代碼

能夠看到,load方法不論分類仍是原類都會被調用,且每一個類只會被調用一次,可是test方法卻只調用了一次。this

test方法只調用一次的緣由在上一篇文章中已經解釋了,分類的方法會放到list的前面,經過msgSend調用方法時會先查找到分類的方法。
load方法爲何會調用兩次呢?咱們仍是要從runtime源碼中尋找答案。spa

runtime 源碼解析

  • 咱們一樣從 objc-os.mm 中的入口函數 _objc_init 入手,這個函數中調用了一個函數 _dyld_objc_notify_register(&map_images, load_images, unmap_image) ,咱們點進load_images函數。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // 判斷是否Categories已經被attach,attach相關詳見上期
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }
 
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;
 
    recursive_mutex_locker_t lock(loadMethodLock);
 
    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }
 
    // Call +load methods (without runtimeLock - re-entrant)
    // 調用 call_load_methods
    call_load_methods();
}
複製代碼
  • 能夠看到調用load方法的函數叫作 call_load_methods(),咱們點進去看一看
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;
 
    loadMethodLock.assertLocked();
 
    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;
 
    void *pool = objc_autoreleasePoolPush();
 
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        // 1. 重複的調用 類 的+load方法直到全部類的load方法都被調用完
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
 
        // 2. Call category +loads ONCE
        // 2. 對每一個 分類 調用一次+load方法
        more_categories = call_category_loads();
        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
 
    objc_autoreleasePoolPop(pool);
 
    loading = NO;
}
複製代碼
  • 這裏就不難看出,原類的 +load 方法必定比分類的 +load 方法要早調用
  • 調用 +load 方法的地方有兩個,一個叫 call_class_loads(),一個叫 call_category_loads(),咱們先點進 call_class_loads()看一看
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
     
    // Detach current loadable list.
    // 留意這一行代碼,後面會用到
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
     
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
 
        // typedef void(*load_method_t)(id, SEL);
        // load_method_t是一個指針類型,這裏建立一個指針指向該類的load方法的地址
        // 能夠看到這裏classes其實是上面的loadable_classes
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue;
 
        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
 
        // 這裏是調用load方法的地方,能夠看到是經過地址找到相應的函數後調用
        (*load_method)(cls, @selector(load));
    }
     
    // Destroy the detached list.
    if (classes) free(classes);
}
複製代碼

這裏就破案了ssr

經過上面的代碼能夠看出,+load 方法的調用不是經過msgSend,而是直接找到該類 +load 方法的地址,經過地址來調用 +load 方法,所以不會出現只調分類的 +load 方法而不調原類的 +load 方法的狀況。指針

有繼承關係時的 +load 調用順序

實驗

咱們一共建立4個類,分別是code

  1. Person
  2. Person(Eat)
  3. Student
  4. Student(Eat)

通過屢次實驗獲得以下結論:

  1. 原類的 +load 方法老是在分類以前被調用(修改Build Phases → Compile Sources 中的順序後也是這樣)
  2. 父類的 +load 方法老是在子類以前被調用(修改順序後也是如此)
  3. 分類的父類 +load 方法可能在 分類的子類的 +load方法後纔會被調用,和Compile Sources中的順序有關

爲了搞清楚這個結論的緣由,咱們仍是要從runtime的源碼入手。

源碼

原類

在上面的 call_class_loads 函數中,是從 loadable_classes 中將函數地址一個一個取出來的,所以咱們須要搞清 loadable_classes 是怎麼來的,以及他的順序是怎麼樣的。
因爲是從 call_class_loads 函數一步一步往回找,所以這裏的調用順序是倒序的,你們要注意這點。

  • 全局搜索一下關鍵字 loadable_classes = ,能夠發如今 add_class_to_loadable_list(Class cls) 中有出現,咱們來看下這個函數的實現:
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;
 
    loadMethodLock.assertLocked();
     
    // 若是沒有實現 +load 方法,那麼不須要添加到list中
    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
     
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load",
                     cls->nameForLogging());
    }
     
    // 能夠看到當list容量不夠時,擴充的邏輯是 *2 + 16
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
     
    // 將class存放到list中,並將method賦值給對應的cls
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
複製代碼
  • 咱們已經知道 add_class_to_loadable_list(cls) 是將該 class 放入list,如今看一下誰調用了 add_class_to_loadable_list,發如今 schedule_class_load(Class cls) 函數中有調用,咱們來看一下這個函數的實現
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize
     
    // #define RW_LOADED             (1<<23)
    // 這裏目測是某個標誌位,猜想表明已經加入到了list
    if (cls->data()->flags & RW_LOADED) return;
 
    // Ensure superclass-first ordering
    // 這裏能夠看出,這個方法是遞歸調用的,每一個類傳進來會先去將他的父類加進來
    schedule_class_load(cls->superclass);
 
    add_class_to_loadable_list(cls);
     
    // 設置標誌位
    cls->setInfo(RW_LOADED);
}
複製代碼
  • 繼續回溯,能夠看到是一個叫作 prepare_load_methods(const headerType *mhdr) 調用了該方法。來看一下這個函數的實現
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
 
    runtimeLock.assertLocked();
 
    // 取到classList
    classref_t const *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 執行schedule_class_load,將有 +load 方法的class加入到 loadable_classes 中
        schedule_class_load(remapClass(classlist[i]));
    }
     
    // 分類相關操做,這裏暫時不看
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
複製代碼
  • 再往上回溯,看看誰調用了 prepare_load_methods
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }
 
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;
 
    recursive_mutex_locker_t lock(loadMethodLock);
 
    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        // 調用 prepare_load_methods
        prepare_load_methods((const headerType *)mh);
    }
 
    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
複製代碼

至此,咱們已經把完整的鏈路打通了,prepare_load_methods 的調用方是 load_images,而 load_images 的調用方是是 objc_init ,咱們來理一下方法的調用順序與做用


那麼原類的 +load 方法調用過程的源碼咱們已經分析完了,接下來讓咱們來看看分類的 +load 方法的調用過程是怎麼樣的

分類

仍是先看一下 prepare_load_methods(const headerType *mhdr) 函數:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
 
    runtimeLock.assertLocked();
 
    // 原類
    classref_t const *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
 
    // 分類
    // _getObjc2NonlazyCategoryList 函數取出了全部分類,這裏沒有作任何順序上的調整,所以是按照編譯順序直接拿到數組
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
 
    // 經過for循環,直接將 categorylist 中的分類加入到loadable_categories中
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
 
        // 添加到 loadable_categories 列表中
        add_category_to_loadable_list(cat);
    }
}
複製代碼

所以能夠知道,分類不會受子父類的關係影響,單純的就是誰先編譯誰先調用 +load 方法,咱們能夠對 Person(Eat) 和 Student(Eat) 調整編譯順序,來驗證咱們的結論。

以下面兩幅圖:



+initialize方法

調用時機

先說結論:

  1. +initialize 在方法第一次給類對象發消息的時候會調用。
  2. +initialize 僅會調用一次。
  3. 子類的 +initialilze 方法調用時會先調用父類的 +initialize 方法。
  4. 若是有分類,那麼調用順序和上一章一張,會調用到後編譯的分類的 +initialize 方法

用上文中的Person, Person(Eat), Student, Student(Eat) 就能夠進行驗證,這裏就不將驗證過程放出來了。
從打印的結果來看是這樣的,可是咱們須要經過源碼進行探究。

源碼

  • 咱們在 objc-class.mm 這個文件中找到 class_getClassmethod 函數
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
     
    // 能夠看到這裏是核心
    return class_getInstanceMethod(cls->getMeta(), sel);
}
複製代碼
  • class_getInstanceMethod 纔是重點,這個函數傳了兩個參數,一個是 cls->getMeta(), 一個是 sel,咱們點進這個函數看一下
/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
 
    // This deliberately avoids +initialize because it historically did so.
 
    // This implementation is a bit weird because it's the only place that
    // wants a Method instead of an IMP.
 
    Method meth;
 
    // 此時尚未給這個cls發過消息,所以不會在cache中找到
    meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
    if (meth == (Method)1) {
        // Cache contains forward:: . Stop searching.
        return nil;
    } else if (meth) {
        return meth;
    }
         
    // Search method lists, try method resolver, etc.
    // 這裏是核心邏輯,注意這裏傳遞的第四個參數是 LOOKUP_INITIALIZE | LOOKUP_RESOLVER
    lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
 
    meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
    if (meth == (Method)1) {
        // Cache contains forward:: . Stop searching.
        return nil;
    } else if (meth) {
        return meth;
    }
 
    return _class_getMethod(cls, sel);
}
複製代碼
  • 這裏的核心函數是 lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER),注意最後一個參數,咱們點進這個函數中看一下,這裏我只放出了關鍵代碼
// 能夠看出 behavior & LOOKUP_INITIALIZE 是真, !cls->isInitialized() 也是真,由於沒有調用過 initialized 方法
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
     
    // 因此能夠走到if判斷裏,這裏是核心邏輯
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again
 
    // If sel == initialize, class_initialize will send +initialize and
    // then the messenger will send +initialize again after this
    // procedure finishes. Of course, if this is not being called
    // from the messenger then it won't happen. 2778172
}
複製代碼
  • 核心函數是 initializeAndLeaveLocked(cls, inst, runtimeLock),咱們一路點進去,最後會進入 initializeAndMaybeRelock,這裏只放出核心邏輯
initializeNonMetaClass(nonmeta);
複製代碼
  • 點進 initializeNonMetaClass 函數看一看,這裏只放出核心代碼
/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
    ASSERT(!cls->isMetaClass());
 
    Class supercls;
    bool reallyInitialize = NO;
 
    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
     
    // 這裏也是遞歸調用
    // 這裏在找是否有super class,而且super class的initialize方法是否被執行過,若是沒執行過,遞歸調用傳遞super class
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
     
    // 這裏是真正調用initialize方法的地方
    callInitialize(cls);
}
複製代碼
  • 至此,咱們終於追蹤到了真正調用 +initialize 方法的地方,讓咱們點進 callInitialize(cls) 函數一探究竟
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}
複製代碼

終於,真相大白了,咱們看到真正調用 +initialize 方法的地方,是經過 objc_msgSend 函數,這也就解釋了爲何會調用到分類的 +initialize 方法,詳情見上一篇文章。

咱們也終於驗證了,父類的 +initialize 方法會先調用,且只會被調用到一次。

寫在後面

但願你們多多上手嘗試,不要以爲看過了就是會了,對於知識須要深層次的挖掘,淺嘗輒止是沒有用的。

文中若有錯誤,歡迎指出。

相關文章
相關標籤/搜索