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

2020-07-21  本文已影响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的时候,是倒序查找类的方法列表中第一个出现的方法,只要找到第一个就直接返回了,所以会出现分类方法覆盖类方法的假象。

上一篇下一篇

猜你喜欢

热点阅读