Objc源码之Load方法实现

Objc源码之对象创建alloc和init
Objc源码之initialize实现
Objc源码之Load方法实现
Objc源码之NSObject和isa
Objc源码之引用计数实现
objc源码之Method消息发送

前言

  Load方法作为OC中一个特殊的方法,在Main函数之前执行,由于这个特性的存在,一方面可以用来在应用执行前,进行一些准备工作,比如用来hook方法,对系统方法进行替换,另一方面,由于Load方法在Mian函数之前执行,如果使用不当,会引起app启动超时、甚至出现crash,使app不能启动,因此要详细了解Load方法,并正确的使用它。
  接下来,我会从源码的角度,来说明一下几个问题:

  • Load方法调用时机
  • 系统如何加载所有类的Load方法
  • 类和分类的Load方法调用顺序。

注:本文分析基于objc4-750源码进行的。

一、Load方法调用时机

1.Load方法调用栈

  首先,我们创建一个对象TestObject,添加Load方法,并在load方法中打上断点,这时我们可以看到下图的调用栈,我们可以看到Load函数是通过动态链接器dyld调用,并最终调用load_images函数,来调用方法中的Load函数,调用栈如下图所示:

Load方法调用栈.png

2.load_images源码分析

load_images中大概分为三部分:
1)判断镜像中是否有Load方法,没有直接返回,通过hasLoadMethods函数完成
2)查找所有的Load方法,通过prepare_load_methods函数完成
3)调用所有Load方法,通过call_load_methods函数完成

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.调用所有Load方法
    call_load_methods();
}

二、系统如何加载所有类的Load方法

1.判断镜像中是否有有Load方法(镜像是一种macho文件)

  从load_images方法,我们可以知道,通过hasLoadMethods方法,来判断是否有Load方法,下面我们看下Load方法的实现:

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方法数量,也就是下面的GETSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");**

2)通过 _getObjc2NonlazyCategoryList获取所有Category中的Load方法数量,也就是下面的GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist") ;

而这两个方法,又是如何读取到类和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方法都是最终调用getDataSection方法,这个方法是从镜像文件中读取响应的代码段,判断具体的是否有load方法, __objc_nlclslist标识的是类+load 函数列表,__objc_nlcatlist标识的是分类中+load 函数列表。从下面代码可以看出,最终是通过读取镜像中的__DATA来找到对应的方法。

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;
}

总结一下整个过程就是:

1.调用hasLoadMethods,找类和Category中的Load方法。具体执行者是_getObjc2NonlazyClassList和_getObjc2NonlazyCategoryList。
2. _getObjc2NonlazyClassList和_getObjc2NonlazyCategoryList又通过调用getDataSection读取__DATA段,来获取是否有Load方法。

2.准备所有加载的load方法

在通过hasLoadMethods判断有Load方法以后,我们需要知道具体有哪些Load方法,读取所有的load方法,这一步是通过prepare_load_methods来完成的,会将所有load方法读取到一个列表中。下面是prepare_load_methods的具体实现:

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

    runtimeLock.assertLocked();

    //获取所有类的Load方法,添加到数组中
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    //获取Category类的Load方法,添加到数组中
    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递归添加数组中,我们知道父类的Load方法是早于子类的方法调用,就是在这一步处理的下面我们看下schedule_class_load方法的具体实现:

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.保证父类优先于子类调用
    schedule_class_load(cls->superclass);

    //3.将类添加到数组中,并标注类方法已添加
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

schedule_class_load中主要做了一下事情:
1.判断类的load方法是否添加过
2.递归调用父类,保证父类先于子类执行。
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.如果内存已满的话,申请现有内存2倍的内存空间
    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内容主要分为以下几步:
1.从class_ro_t中获取load方法
2.如果内存已满,申请现有内存2倍的内存空间
3.保存多有类和方法到loadable_classes中

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

category类中的加载过程和类的耳机在过程基本一致,不同的是,load方法最终会加载到loadable_categories列表中。

总结:

  • 类中的load方法都加载到loadable_classes列表中
  • Category的load方法都加载到loadable_categories列表中

三、类和分类的Load方法调用顺序。

类和category中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. 加载类中的所有方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

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

        // 3. 如果有类或者category中load方法没有加载,继续加载
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

对于 load 方法的调用顺序有两条规则:

  • 类先于分类调用
  • 父类先于子类调用

1)类先于分类调用

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

        // 3. 如果有类或者category中load方法没有加载,继续加载
    } 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.调用分类方法
    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方法时,会判断类的load方法是否已经调用,从而保证类优先于分类加载。

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

2)父类先于子类调用
类的Load方法是在call_class_loads中调用的,我们可以看下源码。

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列表中的数据,进行顺序调用的,所以类和父类的调用顺序与loadable_classes创建时的添加顺序有关,所以我们看下loadable_classes创建的顺序:

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); 
}

从上面代码可以看出,当有父类时,也就是cls->superclass不为空,先添加父类Load方法到loadable_classes中,所以最终父类先于子类调用。

四、Load方法的应用

因为Load方法只会调用一次,并且在Main函数之前执行,所以Load方法可以用于执行一些靠前的操作,比如:Load方法用于hook一些系统方法,像NSArray的objectAtIndex:方法,防止数组越界,避免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

总结一下开头说的三点:

- Load方法调用时机(Main函数之前,通过dyld进行加载)
- 系统如何加载所有类的Load方法(通过读取镜像,获取类列表和Category列表,加载执行)
- 类和分类的Load方法调用顺序。(类先于分类调用,父类先于子类调用)

参考:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容