美文网首页
iOS-底层-+load和+initialize方法

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

作者: Imkata | 来源:发表于2019-11-27 09:12 被阅读0次

    一. +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

    相关文章

      网友评论

          本文标题:iOS-底层-+load和+initialize方法

          本文链接:https://www.haomeiwen.com/subject/fdoewctx.html