iOS原理分析之從源碼看load與initialize方法

iOS原理分析之從源碼看load與initialize方法

1、引言

    在iOS開發中,NSObject類是萬事萬物的基類,其在Objective-C的整理類架構中很是重要,其中有兩個頗有名的方法:load方法與initialize方法。面試

+ (void)load;
+ (void)initialize;

提及這兩個方法,你的第一反應必定是以爲太老套了,這兩個方法的調用時機及做用幾乎成爲了iOS面試的必考題。其自己調用時機也很是簡單:編程

1. load方法在pre-main階段被調用,每一個類都會調用且只會調用一次。緩存

2. initialize方法在類或子類第一次進行方法調用前會調用。安全

上面的兩點說明自己是正確的,可是除此以外,還有許多問題值得咱們深究,例如:架構

1. 子類與父類的load方法的調用順序是怎樣的?函數

2. 類與分類的load方法調用順序是怎樣的?源碼分析

3. 子類未實現load方法,會調用父類的麼?測試

4. 當有多個分類都實現了load方法時,會怎麼樣?spa

5. 每一個類的load方法的調用順序是怎樣的?操作系統

6. 父類與子類的initialize的方法調用順序是怎樣的?

7. 子類實現initialize方法後,還會調用父類的initialize方法麼?

8. 多個分類都實現了initialize方法後,會怎麼樣?

9. ...

如上所提到的問題,你如今都能給出明確的答案麼?其實,load與initialize方法自己還有許多很是有意思的特色,本篇博客,咱們將結合Objective-C源碼,對這兩個方法的實現原理作深刻的分析,相信,若是你對load與initialize還不夠了解,不能徹底明白上面所提出的問題,那麼本篇博客將會使其收穫滿滿。不管在之後的面試中,仍是工做中使用到load和initialize方法時,均可能幫助你從源碼上理解其執行原理。

2、實踐出真知 - 先看load方法

    在開始分析以前,咱們首先能夠先建立一個測試工程,對load方法的執行時機先作一個簡單的測試。首先,咱們建立一個Xcode的命令行程序工程,在其中建立一些類、子類和分類,方便咱們測試,目錄結構以下圖所示:

其中,MyObjectOne和MyObjectTwo都是繼承自NSObject的類,MySubObjectOne是MyObjectOne的子類,MySubObjectTwo是MyObjectTwo的子類,同時咱們還建立了3個分類,在類中實現load方法,並作打印處理,以下:

+ (void)load {
    NSLog(@"load:%@", [self className]);
}

一樣,相似的也在分類中作實現:

+ (void)load {
    NSLog(@"load-category:%@", [self className]);
}

最後咱們在main函數中添加一個Log:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Main");
    }
    return 0;
}

運行工程,打印結果以下:

2021-02-18 14:33:46.773294+0800 KCObjc[21400:23090040] load:MyObjectOne
2021-02-18 14:33:46.773867+0800 KCObjc[21400:23090040] load:MySubObjectOne
2021-02-18 14:33:46.773959+0800 KCObjc[21400:23090040] load:MyObjectTwo
2021-02-18 14:33:46.774008+0800 KCObjc[21400:23090040] load:MySubObjectTwo
2021-02-18 14:33:46.774052+0800 KCObjc[21400:23090040] load-category:MyObjectTwo
2021-02-18 14:33:46.774090+0800 KCObjc[21400:23090040] load-category:MyObjectOne
2021-02-18 14:33:46.774127+0800 KCObjc[21400:23090040] load-category:MyObjectOne
2021-02-18 14:33:46.774231+0800 KCObjc[21400:23090040] Main

從打印結果能夠看出,load方法在main方法開始以前被調用,執行順序上來講,先調用類的load方法,再調用分類的load方法,從父子類的關係上看來,先調用父類的load方法,再調用子類的load方法。

    下面,咱們就從源碼上來分析下,系統如此調用load方法,是源自於什麼樣的奧妙。

3、從源碼分析load方法的調用

    要深刻的研究load方法,咱們首先須要從Objective-C的初始化函數提及:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();

    // 其餘的咱們都不須要關注,只須要關注這行代碼
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

_objc_init函數定義在objc-os.mm文件中,這個函數用來作Objective-C程序的初始化,由引導程序進行調用,其調用實際會很是的早,而且是操做系統引導程序複雜調用驅動,對開發者無感。在_objc_init函數中,會進行環境的初始化,runtime的初始化以及緩存的初始化等等操做,其中很重要的一步操做是執行_dyld_objc_notify_register函數,這個函數會調用load_images函數來進行鏡像的加載。

    load方法的調用,其實就是類加載過程當中的一步,首先,咱們先來看一個load_images函數的實現:

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((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

濾掉其中咱們不關心的部分,與load方法調用相關的核心以下:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // 鏡像中沒有load方法,直接返回
    if (!hasLoadMethods((const headerType *)mh)) return;
    {
        // 準備load方法
        prepare_load_methods((const headerType *)mh);
    }
    // 進行load方法的調用
    call_load_methods();
}

最核心的部分在於load方法的準備與laod方法的調用,咱們一步一步看,先來看load方法的準備(咱們去掉了可有可無的部分):

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
    // 獲取全部類 組成列表
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 將全部類的load方法進行整理
        schedule_class_load(remapClass(classlist[i]));
    }
    // 獲取全部的分類 組成列表
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        // 將分類的load方法進行整理
        add_category_to_loadable_list(cat);
    }
}

看到這裏,咱們基本就有頭緒了,load方法的調用順序,基本能夠肯定是由整理過程所決定的,而且咱們能夠發現,類的load方法整理與分類的load方法整理是互相獨立的,所以也能夠推斷其調用的時機也是獨立的。首先咱們先來看類的load方法整理函數schedule_class_load(去掉無關代碼後):

static void schedule_class_load(Class cls)
{
    // 類不存在或者已經加載過load,則return
    if (!cls) return;
    if (cls->data()->flags & RW_LOADED) return;

    // 保證加載順序,遞歸進行父類加載
    schedule_class_load(cls->superclass);
    // 將當前類的load方法加載進load方法列表中
    add_class_to_loadable_list(cls);
    // 將當前類設置爲已經加載過laod
    cls->setInfo(RW_LOADED); 
}

能夠看到,schedule_class_load函數中使用了遞歸的方式演着繼承鏈逐層向上,保證在加載load方法時,先加載父類,再加載子類。add_class_to_loadable_list是核心的load方法整理函數,以下(去掉了無關代碼):

void add_class_to_loadable_list(Class cls)
{
    IMP method;
    // 讀取類中的load方法
    method = cls->getLoadMethod();
    if (!method) return; // 類中沒有實現load方法,直接返回
    // 構建存儲列表及擴容邏輯
    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));
    }
    // 向列表中添加 loadable_class 結構體,這個結構體中存儲了類與對應的laod方法
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    // 標記列表index的指針移動
    loadable_classes_used++;
}

loadable_clas結構體的定義以下:

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

getLoadMetho函數的實現主要是從類中獲取到load方法的實現,以下:

IMP 
objc_class::getLoadMethod()
{
    // 獲取方法列表
    const method_list_t *mlist;
    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        // 遍歷,找到load方法返回
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name);
            if (0 == strcmp(name, "load")) {
                return meth.imp;
            }
        }
    }
    return nil;
}

如今,關於類的load方法的準備邏輯已經很是清晰了,最終會按照先父類後子類的順序將全部類的load方法添加進名爲loadable_classes的列表中,loadable_classes這個名字你要注意一下,後面咱們還會遇到它。

    咱們再來看分類的laod方法準備過程,其與咱們上面介紹的類很是類似,add_category_to_loadable_list函數簡化後以下:

void add_category_to_loadable_list(Category cat)
{
    IMP method;
    // 獲取當前分類的load方法
    method = _category_getLoadMethod(cat);
    if (!method) return;
    // 列表建立與擴容邏輯
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }
    // 將分類與load方法進行存儲
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

能夠看到,最終分類的load方法是存儲在了loadable_categories列表中。

    準備好了load方法,咱們再來分析下load方法的執行過程,call_load_methods函數的核心實現以下:

void call_load_methods(void)
{
    bool more_categories;
    do {
        // 先對 loadable_classes 進行遍歷,loadable_classes_used這個字段能夠理解爲列表的元素個數
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 再對類別進行遍歷調用
        more_categories = call_category_loads();
       
    } while (loadable_classes_used > 0  ||  more_categories);
 
}

call_class_loads函數實現簡化後以下:

static void call_class_loads(void)
{
    int i;
    // loadable_classes列表
    struct loadable_class *classes = loadable_classes;
    // 須要執行load方法個數
    int used = loadable_classes_used;
    // 清理數據
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    // 循環進行執行 循環的循序是從前到後
    for (i = 0; i < used; i++) {
        // 獲取類
        Class cls = classes[i].cls;
        // 獲取對應load方法
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 
        // 執行load方法
        (*load_method)(cls, @selector(load));
    }
}

call_category_loads函數的實現要複雜一些,簡化後以下:

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // 獲取loadable_categories分類load方法列表
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // 從前日後遍歷進行load方法的調用
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;
        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }
    return new_categories_added;
}

如今,我相信你已經對load方法爲什麼類先調用,分類後調用,而且爲什麼父類先調用,子類後調用。可是還有一點,咱們不甚明瞭,即類之間或分類之間的調用順序是怎麼肯定的,從源碼中能夠看到,類列表是經過_getObjc2NonlazyClassList函數獲取的,一樣分類的列表是經過_getObjc2NonlazyCategoryList函數獲取的。這兩個函數獲取到的類或分類的順序其實是與類源文件的編譯順序有關的,以下圖所示:

能夠看到,打印的load方法的執行順序與源代碼的編譯順序是一直的。

4、initialize方法分析

    咱們能夠採用和分析load方法時同樣的策略來對initialize方法的執行狀況,進行測試,首先將測試工程中全部類中添加initialize方法的實現。此時若是直接運行工程,你會發現控制檯沒有任何輸出,這是因爲只有第一次調用類的方法時,纔會執行initialize方法,在main函數中編寫以下測試代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [MySubObjectOne new];
        [MyObjectOne new];
        [MyObjectTwo new];
        NSLog(@"------------");
        [MySubObjectOne new];
        [MyObjectOne new];
        [MyObjectTwo new];
    }
    return 0;
}

運行代碼控制檯打印效果以下:

2021-02-18 21:29:55.761897+0800 KCObjc[43834:23521232] initialize-cateOne:MyObjectOne
2021-02-18 21:29:55.762526+0800 KCObjc[43834:23521232] initialize:MySubObjectOne
2021-02-18 21:29:55.762622+0800 KCObjc[43834:23521232] initialize-cate:MyObjectTwo
2021-02-18 21:29:55.762665+0800 KCObjc[43834:23521232] ------------

能夠看到,打印數據都出如今分割線前,說明一旦一個類的initialize方法被調用後,後續再向這個類發送消息,也不會在調用initialize方法,還有一點須要注意,須要注意,若是對子類發送消息,父類的initialize會先調用,再調用子類的initialize,同時,分類中若是實現了initialize方法則會覆蓋類自己的,而且分類的加載順序靠後的會覆蓋以前的。下面咱們就經過源碼來分析下initialize方法的這種調用特色。

    首先,在調用類的類方法時,會執行runtime中的class_getClassMethod方法來尋找實現函數,這個方法在源碼中的實現以下:

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    return class_getInstanceMethod(cls->getMeta(), sel);
}

經過源碼能夠看到,調用一個類的類方法,其實是調用其元類的示例方法,getMeta函數用來獲取類的元類,關於類和元類的相關組織原理,咱們這裏先不擴展。咱們須要關注的是class_getInstanceMethod這個函數,這個函數的實現也很是簡單,以下:

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // 作查詢方法列表,嘗試方法解析相關工做
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

    // 從類對象中獲取方法
    return _class_getMethod(cls, sel);
}

在class_getInstanceMethod方法的實現中,_class_getMethod是最終獲取要調用的方法的函數,在這以前,lookUpImpOrForward函數會作一些前置操做,其中就有initialize函數的調用邏輯,咱們去掉無關的邏輯,lookUpImpOrForward中核心的實現以下:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    IMP imp = nil;
    // 核心在於!cls->isInitialized() 若是當前類未初始化過,會執行initializeAndLeaveLocked函數
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    return imp;
}

initializeAndLeaveLocked會直接調用initializeAndMaybeRelock函數,以下:

static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}

initializeAndMaybeRelock函數中會作類的初始化邏輯,這個過程是線程安全的,其核心相關代碼以下:

static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    // 若是已經初始化過,直接返回
    if (cls->isInitialized()) {
        return cls;
    }
    // 找到當前類的非元類
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
    // 進行初始化操做
    initializeNonMetaClass(nonmeta);

    return cls;
}

initializeNonMetaClass函數會採用遞歸的方式沿着繼承鏈向上查詢,找到全部未初始化過的父類進行初始化,核心實現簡化以下:

void initializeNonMetaClass(Class cls)
{
    Class supercls;
    // 標記是否須要初始化
    bool reallyInitialize = NO;
    // 父類若是存在,而且沒有初始化過,則遞歸進行父類的初始化
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    
    SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
    {
        // 若是當前不是正在初始化,而且當前類沒有初始化過
        if (!cls->isInitialized() && !cls->isInitializing()) {
            // 設置初始化標誌,此類標記爲初始化過
            cls->setInitializing();
            // 標記須要進行初始化
            reallyInitialize = YES;
        }
    }
    // 是否須要進行初始化
    if (reallyInitialize) {
        @try
        {
            // 調用初始化函數
            callInitialize(cls);
        }
        @catch (...) {
            @throw;
        }
        return;
    }
}

callInitialize函數最終會調用objc_msgSend函數來向類發送initialize初始化消息,以下:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}

須要注意,initialize方法與load方法最大的區別在於其最終是經過objc_msgSend來實現的,每一個類若是未初始化過,都會經過objc_msgSend來向類發送一次initialize消息,所以,若是子類沒有對initialize實現,按照objc_msgSend的消息機制,其是會沿着繼承鏈一路向上找到父類的實現進行調用的,全部initialize方法並非只會被調用一次,假如父類中實現了這個方法,而且它有多個未實現此方法的子類,則當每一個子類第一次接受消息時,都會調用一遍父類的initialize方法,這點很是重要,在實際開發中必定要牢記。

5、結語

    load和initialize方法是iOS開發中很是簡單也也很是經常使用的兩個方法,然而其與普通的方法比起來,還有有一些特殊,經過對源碼的解讀,咱們能夠更加深入的理解這些特殊之處的緣由及原理,編程的過程就像修行,知其然也知其因此然,與你們共勉。

專一技術,熱愛生活,交流技術,也作朋友。

——琿少 QQ羣:805263726

相關文章
相關標籤/搜索