iOS-底层-+load和+initialize方法

一. +load方法

1. +load方法调用顺序

调用时机:+load方法会在Runtime加载类、分类时调用
调用顺序:先调用父类的+load,后调用子类的+load,再调用分类的+load,并且先编译的先调用
调用方式:根据函数地址直接调用
调用次数:每个类、分类的+load方法,在程序运行过程中只调用一次

首先创建MJStudent继承于MJPerson,给这两个类分别创建两个分类,在类和他们的两个分类中都重写+load方法,在+load方法中打印,代码可见文末Demo。

类和分类创建好之后,其他一行代码不写,运行项目,打印结果如下:

MJPerson +load
MJPerson (Test1) +load
MJPerson (Test2) +load
---------------

发现类和分类的+load方法都有打印。这是因为系统运行过程中只要有这个类或者分类就会调用+load方法,不管你有没有使用,而且只会调用一次。

2. 验证

+load方法的这一点和其他重写方法不一样,在Category分类中我们知道,如果重写有相同的方法,会先调用分类的方法,后调用类的方法,并且如果不同分类中有相同的方法,后编译的分类的方法会先调用。

为了验证不同,我们在MJPerson和它的两个分类里面都写上+test方法,然后执行如下代码:

NSLog(@"---------------");
[MJPerson test];

编译顺序如下:

编译顺序.png

打印结果:

MJPerson +load
MJStudent +load
MJCat +load
MJDog +load
MJPerson (Test2) +load
MJStudent (Test2) +load
MJPerson (Test1) +load
MJStudent (Test1) +load
---------------
MJPerson (Test1) +test

验证结果:

  1. +load方法都在---------之前,验证了,验证了load方法会在Runtime加载类、分类时调用。
  2. 对于+load方法,的确是先调用父类的,后调用子类的,再调用分类的,并且先编译的先调用,而且每个类和分类的+load方法都会调用。
  3. 每个类、分类的+load方法,在程序运行过程中只调用一次。
  4. 对于+test方法,虽然类和分类中都重写了,但是MJPerson (Test1)是最后编译的,所以会先调用它的+test方法,其他方法被覆盖了。

3. 源码分析

首先我们通过以下方法获取MJPerson类的所有方法

//打印类对象里面所有的方法
void printMethodNamesOfClass(Class cls)
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[I];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

执行代码:

printMethodNamesOfClass(object_getClass([MJPerson class])); //传入元类对象

打印如下:

MJPerson load, test, load, test, load, test,

可以发现所有分类的方法都被加载MJPerson中,但是为什么都调用的是自己的呢?

下面通过分析objc4源码分析一下:

+load方法源码分析:

objc4源码解读过程:
objc-os.mm文件

_objc_init (运行时入口)

load_images (加载模块)

prepare_load_methods (准备load方法)
schedule_class_load (规划一些任务)
add_class_to_loadable_list
add_category_to_loadable_list

call_load_methods (调用load方法)
call_class_loads (调用类的load方法)
call_category_loads (再调用分类的load方法)
(*load_method)(cls, SEL_load)

由于源码阅读比较复杂,可按照上面的顺序来阅读,这里只贴上核心的代码:

prepare_load_methods方法:

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

    runtimeLock.assertWriting();

    //获取非懒加载的类(需要加载的类)的列表,然后再调用schedule_class_load方法
    //所以:先编译的类先调用
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //定制、规划一些类的任务
        schedule_class_load(remapClass(classlist[i]));
    }

    //获取非懒加载的分类(需要加载的分类)的列表
    //所以:先编译的分类先调用
    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);
    }
}

上面的方法,主要是根据编译先后获取可加载的类列表和可加载的分类列表,这两个列表会在call_class_loads和call_category_loads里面用到。

可加载的类获取完成后,会进入schedule_class_load方法:

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

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

    //这个方法是递归调用,调用之前会先把父类传进来调用,然后放到loadable_list数组里面,直到没有父类
    //所以:才会先调用父类的load方法,后调用子类的load方法
    schedule_class_load(cls->superclass);

    //添加类到可加载列表里去
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

这个方法采用了递归调用,所以会先把父类添加到可加载类列表里面,再把子类添加到可加载类列表里面。所以最后会先调用父类的load方法,后调用子类的load方法。

可加载类列表和可加载分类列表准备完毕,下面就进入调用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
        while (loadable_classes_used > 0) { 
            call_class_loads(); //先调用类的+load方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads(); //再调用分类的+load方法

        // 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方法。

进入call_class_loads方法,这个方法需要获取可加载的类的列表,这个列表就是在prepare_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;
        //直接取出类里面load方法
        //这个指针直接指向类里面load方法的内存地址
        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, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

上面代码可知,直接取出类里面的load方法进行调用的。
并且从可加载类列表里面取的时候也是从0开始取,所以先编译的类的load方法会先调用。

其中loadable_class这个结构体是可加载的类,里面就一个load方法的实现,这个结构体是专门给load方法使用的,如下:

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

//解释同上
struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

再进入call_category_loads方法,这个方法也需要获取可加载的分类的列表,这个列表也是在prepare_load_methods里面获取的。

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    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;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        //直接取出某一个分类的load方法,拿到内存地址
        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;
        }
    }
......
}

可以看出,分类的load方法也是直接取出,直接调用。
并且从可加载分类列表里面取的时候也是从0开始取,所以先编译的分类的load方法会先调用。

总结:

load方法调用之前:

  1. 先根据编译前后顺序获取可加载类列表
    先把父类添加到可加载类列表里面再把子类添加到可加载类列表里面
  2. 再根据编译前后顺序获取可加载分类列表
  3. load方法调用的时候,从可加载列表从0开始取出类或分类,直接取出它们的load方法进行调用。
  4. +load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用(通过isa和superclass找方法),所以不会存在方法覆盖的问题。
注意:

上面我们都没有主动调用过load方法,都是让系统自动调用,系统会根据load方法地址,直接调用。如果我们主动调用了load方法,那走的就是objc_msgSend函数调用(通过isa和superclass找方法)这一套了,具体可以自己想想流程。

二. +initialize方法

1. +initialize方法调用顺序

调用时机:+initialize方法会在类第一次接收到消息时调用(走的也是objc_msgSend这一套机制)
调用顺序:先调用父类的+initialize,再调用子类的+initialize(先初始化父类,再初始化子类)
调用方式:通过objc_msgSend调用
调用次数:每个类只会初始化一次

2. 验证

下面用代码验证一下上面的结论,首先创建MJStudent继承于MJPerson,给这两个类分别创建两个分类,在类和他们的两个分类中都重写+initialize方法,在+initialize方法中打印。再创建MJTeacher继承于MJPerson,不重写任何方法。代码可见文末Demo。

执行如下代码:

[MJStudent alloc];
[MJStudent alloc];
[MJStudent alloc];
[MJTeacher alloc];

打印结果如下:

MJPerson (Test2) +initialize
MJStudent (Test1) +initialize
MJPerson (Test2) +initialize

可以发现,MJStudent初始化的时候会先调用MJPerson的initialize,再调用自己的initialize,而且无论发送多少次消息,MJStudent只会初始化一次。MJTeacher初始化的时候,由于它自己没实现initialize方法,所以会去调用MJPerson的initialize方法。

总结:

  1. 先调用父类的initialize方法再调用子类的initialize方法,而且一个类只会初始化一次。
  2. 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)。
  3. 如果分类实现了+initialize,就会覆盖类本身的+initialize调用。

3. 源码分析

下面我们通过查看objc4源码看一下为什么是这样:
+initialize方法源码分析:

objc4源码解读过程:
objc-msg-arm64.s文件

objc_msgSend

objc-runtime-new.mm文件

class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)

既然+initialize方法是在类第一次接收到消息时调用,我们就先看看objc_msgSend方法里面有没有做什么事,首先在objc4里面搜索“objc_msgSend(”,可以发现objc_msgSend函数底层是通过汇编实现的,汇编看不懂,我们就自己先回顾一下objc_msgSend内部寻找方法流程:

isa -> 类对象\元类对象,寻找方法,如果找到方法就调用,如果找不到👇
superclass -> 类对象\元类对象,寻找方法,如果找到方法就调用,如果找不到👇
superclass -> 类对象\元类对象,寻找方法,如果找到方法就调用,如果找不到👇
superclass -> 类对象\元类对象,寻找方法,如果找到方法就调用,如果找不到👇
superclass -> 类对象\元类对象,寻找方法,如果找到方法就调用,如果找不到👇

更多关于方法寻找流程,可参考isa指针和superclass指针

直接查看objc_msgSend源码这条路走不通,我们就换个方向,找class_getInstanceMethod方法的内部实现,这个函数传入一个类对象,在类对象中寻找对象方法,是C语言写的。

同样,在objc4搜索“class_getInstanceMethod(”,找到如下方法:

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.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

当然,我们也可以搜索“class_getClassMethod(”,查看寻找类方法的内部实现:

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

    return class_getInstanceMethod(cls->getMeta(), sel);
}

可以发现,这个方法内部也是调用class_getInstanceMethod,只不过传入的不是类对象而是元类对象,这和我们以前说的“实例对象和元类对象的内存结构是一样的”相吻合。

在class_getInstanceMethod方法中进入lookUpImpOrNil

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

再进入lookUpImpOrForward

......
 //initialize是否需要初始化   !cls->isInitialized这个类没有初始化
 if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }
......

上面会判断如果需要初始化并且这个类没有初始化,就进入_class_initialize方法进行初始化,验证了,一个类只初始化一次。

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());
 
    Class supercls;
    bool reallyInitialize = NO;

    //如果有父类,并且父类没有初始化就递归调用,初始化父类
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
......
    //没有父类或者父类已经初始化,开始初始化子类
    callInitialize(cls); //初始化子类
......

上面会先判断如果有父类并且父类没有初始化就递归调用,初始化父类,如果没有父类或者父类已经初始化,就开始初始化子类。验证了,先初始化父类,再初始化子类。
进入callInitialize, 开始初始化这个类

void callInitialize(Class cls)
{
    //第一个参数是类,第二个参数是SEL_initialize消息
    //就是给某个类发送SEL_initialize消息
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

通过上面的源码分析,可以知道,的确是先调用父类的Initialize再调用子类的Initialize,并且一个类只会初始化一次。

面试题:

问题一:+load方法和+ Initialize方法的区别是什么?

  1. 调用时机:load是在Runtime加载类、分类的时候调用,只会调用一次,Initialize是在类第一次接收到消息时调用,每一个类只会初始化一次。
  2. 调用方式:load是根据函数地址直接调用,Initialize是通过objc_msgSend调用。

问题二:说一下load和Initialize的调用顺序?

对于load:先调用父类的+load,后调用子类的+load,再调用分类的+load,并且先编译的先调用
对于Initialize:先调用父类的+initialize,再调用子类的+initialize(先初始化父类,再初始化子类)

Demo地址:load和Initialize

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

推荐阅读更多精彩内容