美文网首页
iOS进阶专项分析(九)、load与initialize,类与分

iOS进阶专项分析(九)、load与initialize,类与分

作者: 溪浣双鲤 | 来源:发表于2020-07-21 14:04 被阅读0次

    先来看一个升级版面试题:

    1、load与initialize分别是何时调用的?以及load与initialize这两个方法的在父类,子类,分类之间的调用顺序是怎样的?
    2、分类实现了类的initialize方法,那么类的方法initialize还会调用吗?为什么?

    针对这个面试题,我们继续深入底层,本篇文章结构:

    1. load函数与initialize函数调用时机
    2. 类(Class)的方法和分类(Category)的方法之间的调用关系(为什么分类的方法会覆盖类的方法)
    3. 面试题答案(笔者总结,仅供参考)

    一、load函数与initialize函数调用时机及顺序


    新建工程,实现父类BMPerson、子类BMStudent和子类的分类BMStudent(Cover),分别重写这三个类的load以及initialize,在main函数里面也做个函数打印,运行后打印结果如下

    load及initialize调用时机顺序.png

    从打印结果我们粗略的能看出:

    不管是子类,父类还是分类,load方法的调用都在main函数之前就已经调用了

    而initialize方法则是在main函数之后,也就是程序运行的时候才开始调用

    先来看load,结合笔者上篇深入App启动之dyld、map_images、load_images,我们其实知道:

    load方法调用时机其实就是在程序运行,Runtime进行load_images时调用的,在main函数之前,父类子类分类的调用顺序是:先调用类,后调用所有分类;调用类会先递归调用父类,后调用子类;分类和类的调用顺序没有关系,是根据Mach-O文件的顺序进行调用的。

    接下里我们分析initialize的调用时机及调用关系。

    由于我们同时打印父类,子类,分类发现子类的并不调用,接下来我们注释掉分类的initialize,查看打印结果:

    注释掉分类的initialize.png

    然后在子类的initialize中打上断点,查看函数调用堆栈:

    initialize子类调用堆栈.png

    利用控制变量的思想,从以上的所有打印结果,我们能得出:

    1、子类父类分类的调用顺序是:如果实现了分类:先父类后分类,并且不再调用原来子类中的initialize;如果没有实现分类:先父类后子类

    2、initialize方法调用时机是在Class对象进行初始化时,通过Runtime的消息转发机制,查找方法的imp然后进行调用的,对比load方法,它是在main函数之后,对象创建初始化的时候调用的。

    那么问题来了:为什么分类的initialize会覆盖类的initialize呢?接下来我们从源码进行分析

    二、类(Class)的方法和分类(Category)的方法之间的调用关系(为什么分类的方法会覆盖类的方法)


    先思考:为什么分类的方法会覆盖类的方法呢?我们知道方法调用底层就是通过Runtime进行消息转发,去对应类的methodList进行方法编号imp查找,然后调用 而且上一篇深入App启动之dyld、map_images、load_imagesmap_images进行分析过,在类的结构中方法都存储在datamethods方法表里面,这个表的类型是method_list_tmethod_list_t的父类list_array_tt会提供attachLists方法把分类的方法都添加到类里面,中间也没有进行任何去重这种敏感的操作,而且从Mach-O文件中我们也能看出:类的方法并没有被分类覆盖掉,这类的initialize方法以及分类的initialize方法的地址也不一样,这两个方法都还存在。

    Mach-O文件查看方法地址.png

    既然存的时候,都存进去了,那么只有一种可能:在方法调用的时候,肯定做了只会读分类的方法的逻辑操作!

    从上面断点打印的调用堆栈信息,我们直接进入Objc源码搜索lookUpImpOrForward,代码如下

    IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                           bool initialize, bool cache, bool resolver)
    {
        IMP imp = nil;
        bool triedResolver = NO;
    
        runtimeLock.assertUnlocked();
    
        //1、先从缓存查找,如果有就取出来cache_getImp;缓存没有,先看类是否实现,如果没实现就去实现并初始化
        // Optimistic cache lookup
        if (cache) {
            imp = cache_getImp(cls, sel);
            if (imp) return imp;
        }
    
        ......
    
        runtimeLock.lock();
        checkIsKnownClass(cls);
    
        if (!cls->isRealized()) {
            realizeClass(cls);
        }
    
        if (initialize  &&  !cls->isInitialized()) {
            runtimeLock.unlock();
            _class_initialize (_class_getNonMetaClass(cls, inst));
            runtimeLock.lock();
        }
        
        
        //2.开始retry查找
        
     retry:    
        runtimeLock.assertLocked();
    
        // Try this class's cache.
        //从这个类的缓存中查找
        imp = cache_getImp(cls, sel);
        if (imp) goto done;
    
        // Try this class's method lists.
        //
        {
            Method meth = getMethodNoSuper_nolock(cls, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, cls);
                imp = meth->imp;
                goto done;
            }
        }
    
        // Try superclass caches and method lists.
        //从父类的缓存以及方法列表里面进行查找
        {
            
            ......
            
        }
    
        // No implementation found. Try method resolver once.
        //没有找到,尝试一次动态方法解析_class_resolveMethod,方法还是找不到imp,看看开发者是否实现预留的方法resolveInstanceMethod或者resolveClassMethod
        if (resolver  &&  !triedResolver) {
            runtimeLock.unlock();
            _class_resolveMethod(cls, sel, inst);
            runtimeLock.lock();
            ......
            
            triedResolver = YES;
            goto retry;
        }
    
        //还是找不到,就进行消息转发,打印方法找不到
        // No implementation found, and method resolver didn't help. 
        // Use forwarding.
    
        imp = (IMP)_objc_msgForward_impcache;
        cache_fill(cls, sel, imp, inst);
    
     done:
        runtimeLock.unlock();
    
        return imp;
    }
    
    
    

    整个方法lookUpImpOrForward的imp查找过程大致就是三步:

    1. 从Optimistic cache缓存中查找
    2. 找不到先判断类是否实现,如果未实现就进行实现
    3. 然后开始retry查找

    retry中的imp查找过程就是

    1. 先查找类的缓存和方法列表
    2. 在查找父类的缓存和方法列表
    3. 以上都找不到就进行一次动态方法解析,查看开发者针对该类有没有实现了设计时预留的方法resolveInstanceMethod或者resolveClassMethod
    4. 如果动态方法解析还找不到就进行消息转发,然后打印方法找不到

    我们的场景主要是查看initialize方法的调用顺序,所以查看第一步,从类里面找就行了。

    找到类方法查找的关键函数getMethodNoSuper_nolock并找到关键函数search_method_list点击进入,下面贴上这两个函数的源码

    static method_t *
    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
        runtimeLock.assertLocked();
    
        assert(cls->isRealized());
        // fixme nil cls? 
        // fixme nil sel?
    
        for (auto mlists = cls->data()->methods.beginLists(), 
                  end = cls->data()->methods.endLists(); 
             mlists != end;
             ++mlists)
        {
            method_t *m = search_method_list(*mlists, sel);
            if (m) return m;
        }
    
        return nil;
    }
    
    
    static method_t *search_method_list(const method_list_t *mlist, SEL sel)
    {
        int methodListIsFixedUp = mlist->isFixedUp();
        int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
        
        if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
            return findMethodInSortedMethodList(sel, mlist);
        } else {
            // Linear search of unsorted method list
            for (auto& meth : *mlist) {
                if (meth.name == sel) return &meth;
            }
        }
    
    #if DEBUG
        // sanity-check negative results
        if (mlist->isFixedUp()) {
            for (auto& meth : *mlist) {
                if (meth.name == sel) {
                    _objc_fatal("linear search worked when binary search did not");
                }
            }
        }
    #endif
    
        return nil;
    }
    

    search_method_list找到关键函数findMethodInSortedMethodList,重点来了!!!!!!!!!!

    static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
    {
        assert(list);
    
        const method_t * const first = &list->first;
        const method_t *base = first;
        const method_t *probe;
        uintptr_t keyValue = (uintptr_t)key;
        uint32_t count;
        
        for (count = list->count; count != 0; count >>= 1) {
            probe = base + (count >> 1);
            
            uintptr_t probeValue = (uintptr_t)probe->name;
            
            if (keyValue == probeValue) {
                // `probe` is a match.
                // Rewind looking for the *first* occurrence of this value.
                // This is required for correct category overrides.
                while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                    probe--;
                }
                return (method_t *)probe;
            }
            
            if (keyValue > probeValue) {
                base = probe + 1;
                count--;
            }
        }
        
        return nil;
    }
    
    

    注意其中for循环中的一段核心代码及注释!这段代码正是category覆盖类方法的关键点!这段代码的逻辑就是:**倒序查找方法的第一次实现 **

    
    if (keyValue == probeValue) {
                // `probe` is a match.
                // Rewind looking for the *first* occurrence of this value.
                // This is required for correct category overrides.
                while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                    probe--;
                }
                return (method_t *)probe;
     }
    
    

    结合之前我们分析map_images加载顺序:先加载父类->再子类->所有类的分类。所以在消息转发查找imp的时候,一定会从表的后边往前边查,而分类中的方法正是最后添加的!所以如果这个类分类也实现了这个方法,一定会先找分类中的方法,这里的逻辑正是分类重写的精髓所在!

    知道了为啥分类中的方法会覆盖类中的方法之后,笔者从源码中也看出了分类方法会覆盖类中的,但是分类之间是没有绝对的先后顺序的,所以我们在为类添加分类的时候需要注意这一点,不然可能会导致分类之间互相影响。

    三、面试题答案(笔者总结,仅供参考)


    1、load与initialize分别是何时初始化的?以及load与initialize这两个方法的在父类,子类,分类之间的调用顺序是怎样的?

    load调用时机

    main函数之前,Runtime进行load_images时调用

    load调用顺序

    父类子类分类的调用顺序是:先调用类,后调用所有分类;调用类会先递归调用父类,后调用子类;分类和类的调用顺序没有关系,是根据Mach-O文件的顺序进行调用的。

    initialize调用时机

    main函数之后,Runtime通过消息转发查找方法的imp,在lookUpImpOrForward时,在类的方法列表中找到并调用

    initialize调用顺序

    如果分类中重写了initialize方法,则调用顺序:先父类后分类
    如果分类未重写initialize方法,则调用顺序:先父类后子类

    2、分类实现了类的initialize方法,那么类的方法initialize还会调用吗?为什么?

    分类中实现的类的initialize方法,那么类的方法就不会调用了。

    之所以出现这种覆盖的假象,是因为map_images操作方法的时候,是先处理类后处理分类的,所以方法存进类的方法的顺序是:先添加类,后添加分类。但是在Runtime查找imp的时候,是倒序查找类的方法列表中第一个出现的方法,只要找到第一个就直接返回了,所以会出现分类方法覆盖类方法的假象。

    相关文章

      网友评论

          本文标题:iOS进阶专项分析(九)、load与initialize,类与分

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