iOS进阶专项分析(九)、load与initialize,类与分类的方法之间的关系

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

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

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