Objc源碼之Load方法實(shí)現(xiàn)

Objc源碼之對(duì)象創(chuàng)建alloc和init
Objc源碼之initialize實(shí)現(xiàn)
Objc源碼之Load方法實(shí)現(xiàn)
Objc源碼之NSObject和isa
Objc源碼之引用計(jì)數(shù)實(shí)現(xiàn)
objc源碼之Method消息發(fā)送

前言

??Load方法作為OC中一個(gè)特殊的方法,在Main函數(shù)之前執(zhí)行,由于這個(gè)特性的存在,一方面可以用來(lái)在應(yīng)用執(zhí)行前,進(jìn)行一些準(zhǔn)備工作,比如用來(lái)hook方法,對(duì)系統(tǒng)方法進(jìn)行替換,另一方面,由于Load方法在Mian函數(shù)之前執(zhí)行,如果使用不當(dāng),會(huì)引起app啟動(dòng)超時(shí)、甚至出現(xiàn)crash,使app不能啟動(dòng),因此要詳細(xì)了解Load方法,并正確的使用它。
??接下來(lái),我會(huì)從源碼的角度,來(lái)說(shuō)明一下幾個(gè)問題:

  • Load方法調(diào)用時(shí)機(jī)
  • 系統(tǒng)如何加載所有類的Load方法
  • 類和分類的Load方法調(diào)用順序。

注:本文分析基于objc4-750源碼進(jìn)行的。

一、Load方法調(diào)用時(shí)機(jī)

1.Load方法調(diào)用棧

??首先,我們創(chuàng)建一個(gè)對(duì)象TestObject,添加Load方法,并在load方法中打上斷點(diǎn),這時(shí)我們可以看到下圖的調(diào)用棧,我們可以看到Load函數(shù)是通過動(dòng)態(tài)鏈接器dyld調(diào)用,并最終調(diào)用load_images函數(shù),來(lái)調(diào)用方法中的Load函數(shù),調(diào)用棧如下圖所示:

Load方法調(diào)用棧.png

2.load_images源碼分析

load_images中大概分為三部分:
1)判斷鏡像中是否有Load方法,沒有直接返回,通過hasLoadMethods函數(shù)完成
2)查找所有的Load方法,通過prepare_load_methods函數(shù)完成
3)調(diào)用所有Load方法,通過call_load_methods函數(shù)完成

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    //1.判斷鏡像中是否有Load方法,沒有直接返回
    if (!hasLoadMethods((const headerType *)mh)) return;
    recursive_mutex_locker_t lock(loadMethodLock);

    // 2.查找所有的Load方法
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }
    // 3.調(diào)用所有Load方法
    call_load_methods();
}

二、系統(tǒng)如何加載所有類的Load方法

1.判斷鏡像中是否有有Load方法(鏡像是一種macho文件)

??從load_images方法,我們可以知道,通過hasLoadMethods方法,來(lái)判斷是否有Load方法,下面我們看下Load方法的實(shí)現(xiàn):

bool hasLoadMethods(const headerType *mhdr)
{
    size_t count;
    if (_getObjc2NonlazyClassList(mhdr, &count)  &&  count > 0) return true;
    if (_getObjc2NonlazyCategoryList(mhdr, &count)  &&  count > 0) return true;
    return false;
}

hasLoadMethods中分為兩步:
1)通過 _getObjc2NonlazyClassList獲取所有類中的Load方法數(shù)量,也就是下面的GETSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");**

2)通過 _getObjc2NonlazyCategoryList獲取所有Category中的Load方法數(shù)量,也就是下面的GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist") ;

而這兩個(gè)方法,又是如何讀取到類和category中是否有Load方法呢,通過下面的方法:

#define GETSECT(name, type, sectname)                                   \
    type *name(const headerType *mhdr, size_t *outCount) {              \
        return getDataSection<type>(mhdr, sectname, nil, outCount);     \
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
    }

//      function name                 content type     section name
GETSECT(_getObjc2NonlazyClassList,    classref_t,      "__objc_nlclslist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *,    "__objc_nlcatlist");

_getObjc2NonlazyClassList_getObjc2NonlazyCategoryList方法都是最終調(diào)用getDataSection方法,這個(gè)方法是從鏡像文件中讀取響應(yīng)的代碼段,判斷具體的是否有l(wèi)oad方法, __objc_nlclslist標(biāo)識(shí)的是類+load 函數(shù)列表,__objc_nlcatlist標(biāo)識(shí)的是分類中+load 函數(shù)列表。從下面代碼可以看出,最終是通過讀取鏡像中的__DATA來(lái)找到對(duì)應(yīng)的方法。

template <typename T>
T* getDataSection(const headerType *mhdr, const char *sectname, 
                  size_t *outBytes, size_t *outCount)
{
    unsigned long byteCount = 0;
    T* data = (T*)getsectiondata(mhdr, "__DATA", sectname, &byteCount);
    if (!data) {
        data = (T*)getsectiondata(mhdr, "__DATA_CONST", sectname, &byteCount);
    }
    if (!data) {
        data = (T*)getsectiondata(mhdr, "__DATA_DIRTY", sectname, &byteCount);
    }
    if (outBytes) *outBytes = byteCount;
    if (outCount) *outCount = byteCount / sizeof(T);
    return data;
}

總結(jié)一下整個(gè)過程就是:

1.調(diào)用hasLoadMethods,找類和Category中的Load方法。具體執(zhí)行者是_getObjc2NonlazyClassList和_getObjc2NonlazyCategoryList。
2. _getObjc2NonlazyClassList和_getObjc2NonlazyCategoryList又通過調(diào)用getDataSection讀取__DATA段,來(lái)獲取是否有Load方法。

2.準(zhǔn)備所有加載的load方法

在通過hasLoadMethods判斷有Load方法以后,我們需要知道具體有哪些Load方法,讀取所有的load方法,這一步是通過prepare_load_methods來(lái)完成的,會(huì)將所有l(wèi)oad方法讀取到一個(gè)列表中。下面是prepare_load_methods的具體實(shí)現(xiàn):

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    //獲取所有類的Load方法,添加到數(shù)組中
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    //獲取Category類的Load方法,添加到數(shù)組中
    category_t **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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

hasLoadMethods主要分為兩部分:

1)獲取所有類的Load方法,添加到列表(loadable_classes)中。

這一步是通過_getObjc2NonlazyClassList獲取類列表,然后通過schedule_class_load遞歸添加數(shù)組中,我們知道父類的Load方法是早于子類的方法調(diào)用,就是在這一步處理的下面我們看下schedule_class_load方法的具體實(shí)現(xiàn):

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize
    //1.判斷類的load方法是否添加過
    if (cls->data()->flags & RW_LOADED) return;

    // 2.保證父類優(yōu)先于子類調(diào)用
    schedule_class_load(cls->superclass);

    //3.將類添加到數(shù)組中,并標(biāo)注類方法已添加
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

schedule_class_load中主要做了一下事情:
1.判斷類的load方法是否添加過
2.遞歸調(diào)用父類,保證父類先于子類執(zhí)行。
3.將類通過add_class_to_loadable_list添加到loadable_classes中。
add_class_to_loadable_list源碼如下:

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();
    
    //1.從class_ro_t中獲取load方法
    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());
    }
    
    //2.如果內(nèi)存已滿的話,申請(qǐng)現(xiàn)有內(nèi)存2倍的內(nèi)存空間
    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));
    }
    
    //3.保存多有類和方法到loadable_classes中
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

add_class_to_loadable_list內(nèi)容主要分為以下幾步:
1.從class_ro_t中獲取load方法
2.如果內(nèi)存已滿,申請(qǐng)現(xiàn)有內(nèi)存2倍的內(nèi)存空間
3.保存多有類和方法到loadable_classes中

2)獲取Category類的Load方法,添加到列表(loadable_categories)中。

category類中的加載過程和類的耳機(jī)在過程基本一致,不同的是,load方法最終會(huì)加載到loadable_categories列表中。

總結(jié):

  • 類中的load方法都加載到loadable_classes列表中
  • Category的load方法都加載到loadable_categories列表中

三、類和分類的Load方法調(diào)用順序。

類和category中l(wèi)oad方法,都是通過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. 加載類中的所有方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 加載category中的所有方法
        more_categories = call_category_loads();

        // 3. 如果有類或者category中l(wèi)oad方法沒有加載,繼續(xù)加載
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

對(duì)于 load 方法的調(diào)用順序有兩條規(guī)則:

  • 類先于分類調(diào)用
  • 父類先于子類調(diào)用

1)類先于分類調(diào)用

    do {
        // 1. 加載類中的所有方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
        // 2. 加載category中的所有方法
        more_categories = call_category_loads();

        // 3. 如果有類或者category中l(wèi)oad方法沒有加載,繼續(xù)加載
    } while (loadable_classes_used > 0  ||  more_categories);

從上面代碼可以看出,類的load方法先于Category中的load方法,也就是類先于分類。
如果category的鏡像先于類的鏡像加載,那么怎么保證類先于分類加載呢???
我們看下call_category_loads的源碼

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // 1.獲取分類列表
    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;

    // 2.調(diào)用分類方法
    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()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }
    ...
    return new_categories_added;
}

從上面代碼可以看出,在加載Category的load方法時(shí),會(huì)判斷類的load方法是否已經(jīng)調(diào)用,從而保證類優(yōu)先于分類加載。

        if (cls  &&  cls->isLoadable()) {
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }

2)父類先于子類調(diào)用
類的Load方法是在call_class_loads中調(diào)用的,我們可以看下源碼。

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;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

我們可以看出,call_class_loads通過獲取loadable_classes列表中的數(shù)據(jù),進(jìn)行順序調(diào)用的,所以類和父類的調(diào)用順序與loadable_classes創(chuàng)建時(shí)的添加順序有關(guān),所以我們看下loadable_classes創(chuàng)建的順序:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // 有父類,先添加父類Load方法到loadable_classes中
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

從上面代碼可以看出,當(dāng)有父類時(shí),也就是cls->superclass不為空,先添加父類Load方法到loadable_classes中,所以最終父類先于子類調(diào)用。

四、Load方法的應(yīng)用

因?yàn)長(zhǎng)oad方法只會(huì)調(diào)用一次,并且在Main函數(shù)之前執(zhí)行,所以Load方法可以用于執(zhí)行一些靠前的操作,比如:Load方法用于hook一些系統(tǒng)方法,像NSArray的objectAtIndex:方法,防止數(shù)組越界,避免crash。

#import <objc/runtime.h>

@implementation NSArray (Safe)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(objectAtIndex:);
        SEL swizzledSelector = @selector(xxx_objectAtIndex:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_objectAtIndex:(NSInteger)index {
    [self xxx_objectAtIndex:index];
    NSLog(@"objectAtIndex: %@", self);
}

@end

總結(jié)一下開頭說(shuō)的三點(diǎn):

- Load方法調(diào)用時(shí)機(jī)(Main函數(shù)之前,通過dyld進(jìn)行加載)
- 系統(tǒng)如何加載所有類的Load方法(通過讀取鏡像,獲取類列表和Category列表,加載執(zhí)行)
- 類和分類的Load方法調(diào)用順序。(類先于分類調(diào)用,父類先于子類調(diào)用)

參考:

objc4-750源碼
你真的了解 load 方法么?
NSObject +load and +initialize - What do they do?
method-swizzling

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

推薦閱讀更多精彩內(nèi)容