iOS底层原理探索— Runtime之消息机制

探索底层原理,积累从点滴做起。大家好,我是Mars。

往期回顾

iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
iOS底层原理探索— block的本质(一)
iOS底层原理探索— block的本质(二)
iOS底层原理探索— Runtime之isa的本质
iOS底层原理探索— Runtime之class的本质

今天继续带领大家探索iOS之Runtime的本质。

前言

OC是一门动态性比较强的编程语言,它的动态性是基于RuntimeAPIRuntime在我们的实际开发中占据着重要的地位,在面试过程中也经常遇到Runtime相关的面试题,我们在之前几期的探索分析时也经常会到Runtime的底层源码中查看相关实现。Runtime对于iOS开发者的重要性不言而喻,想要学习和掌握Runtime的相关技术,就要从Runtime底层的一些常用数据结构入手。掌握了它的底层结构,我们学习起来也能达到事半功倍的效果。今天研究OC消息机制

消息机制

OC语言中方法调用通过消息机制来实现,方法调用其实都是转换为 objc_msgSend函数调用。

objc_msgSend函数.png

OC消息机制可以分为一下三个阶段:

1、消息发送阶段:从类及父类的方法缓存列表及方法列表查找方法;

2、动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现;

3、消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理;

如果消息转发也没有实现,就会报出经典的错误:unrecognzied selector sent to instance,方法找不到的错误,无法识别消息。

接下来我们通过源码分析消息机制的三个阶段分别是如何实现的。

1、消息发送

在项目中方法调用的频率很高,所以为了能够提升效率,在底层代码中objc_msgSend函数的实现是通过汇编语言编写的,我们在源码中找到objc-msg-arm64.s汇编文件,来具体分析一下objc_msgSend函数的实现。

objc_msgSend内部实现.png

objc_msgSend函数中首先判断消息接收者receiver是否为空。如果传入的消息接受者为nil则会执行LNilOrTaggedLNilOrTagged内部会执行LReturnZero,而LReturnZero内部则直接return 0

如果传入的消息接收者receiver不为空则通过消息接收者receiverisa指针找到消息接收者的class,执行CacheLookup从方法缓存中取查找。如果在方法缓存列表找到则执行CacheHit,调用方法或者返回函数地址;如果找到就执行CheckMissCheckMiss内调用__objc_msgSend_uncached,方法没有被缓存。

__objc_msgSend_uncached内会执行MethodTableLookup,去方法列表中查找。MethodTableLookup内部的核心代码__class_lookupMethodAndLoadCache3也就是c语言函数_class_lookupMethodAndLoadCache3(双下划线开头变成单下划线)。

以上分析我们用简单的流程图来总结:

消息发送阶段流程.png

接下来我们进入_class_lookupMethodAndLoadCache3函数,分析是如何从方法列表中查找方法。

_class_lookupMethodAndLoadCache3函数

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

函数内部调用lookUpImpOrForward方法,传入三个BOOL类型的参数。

lookUpImpOrForward 函数

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //接收传入的参数, initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    // 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    // 防止动态添加方法,缓存会变化,再次查找缓存。
    imp = cache_getImp(cls, sel);
    // 如果找到imp方法地址, 直接调用done, 返回方法地址
    if (imp) goto done;

    // 查找方法列表, 传入类对象和方法名
    {
        // 根据sel去类对象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果方法存在,则缓存方法,
            // 内部调用的就是 cache_fill。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 方法缓存之后, 取出函数地址imp并返回
            imp = meth->imp;
            goto done;
        }
    }

    // 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
    {
        unsigned attempts = unreasonableClassCount();
        // 如果父类缓存列表及方法列表均找不到方法,则去父类的父类去查找。
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 查找父类的缓存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 执行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出循环, 停止搜索
                    break;
                }
            }
            
            // 查找父类的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 同样拿到方法, 在本类进行缓存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 执行done, 返回imp
                goto done;
            }
        }
    }
    
    // ---------------- 消息发送阶段完成,没有找到方法实现,进入动态解析阶段 ---------------------
    //首先检查是否已经被标记为动态方法解析,如果没有才会进入动态方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        //将triedResolver标记为YES,下次就不会再进入动态方法解析
        triedResolver = YES;
        goto retry;
    }

    // ---------------- 动态解析阶段完成,进入消息转发阶段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    // 返回方法地址
    return imp;
}

getMethodNoSuper_nolock 函数

方法列表中查找方法

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二维数组
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
         // mlists 为 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

getMethodNoSuper_nolock函数中通过遍历方法列表拿到method_list_t最终通过search_method_list函数查找方法
search_method_list函数

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 {
        //  如果方法列表没有排序好就遍历查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}

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;
    // >>1 表示将变量n的各个二进制位顺序右移1位,最高位补二进制0。
    // count >>= 1 如果count为偶数则值变为(count / 2)。如果count为奇数则值变为(count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向数组中间的值
        probe = base + (count >> 1);
        // 取出中间method_t的name,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 则折半向后查询
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

通过以上分析,我们了解了消息机制中的第一阶段消息发送阶段,下面我们用一张图来总结一下整体流程:

消息发送.png

2、动态方法解析

当在类和父类的方法缓存列表、方法列表中都找不到方法时,就会进入动态方法解析阶段。我们在消息发送阶段源码中看到,进入动态方法解析阶段是通过函数_class_resolveMethod

_class_resolveMethod函数

_class_resolveMethod函数.png

函数内部会根据是元类还是类,并且类方法和对象方法的动态方法解析是调用不同的函数:
动态解析对象方法时,会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法。
动态解析类方法时,会调用+(BOOL)resolveClassMethod:(SEL)sel方法。

动态解析方法之后,会将triedResolver = YES;那么下次就不会在进行动态解析阶段了,之后会回到消息发送阶段,重新执行retry,重新对方法查找一遍。

动态方法解析流程图.png

我们可以利用动态方法解析来动态的添加方法。我们将MPerson类中的test方法实现注释掉,用other方法的实现来替代test方法实现:
动态添加方法.png

从图中的可以看到,我们注释掉test方法实现后系统已经报出了警告,下面我们测试一下代码:
测试动态添加方法.png

当调用MPersontest方法时,打印了[MPerson other]。动态添加方法成功。

这里需要注意class_addMethod函数用来向具有给定名称和实现的类添加新方法,class_addMethod将添加一个方法实现的覆盖,但是不会替换已有的实现。也就是说如果上述代码中已经实现了-(void)test方法,则不会再动态添加方法。

3、消息转发阶段

如果上面两个阶段都失败的话,就会来到第三阶段:消息转发阶段。
由于OC中消息机制并不是开源的,这里就直接将消息转发的原理告诉给大家了。

进入消息转发阶段后,就会判断是否指定了其它对象来执行方法。具体查看当前类是否实现了forwardingTargetForSelector函数,如果返回值不为空,那么说明指定了转发目标,那么就会让转发目标处理消息。

如果forwardingTargetForSelector函数返回为nil,没有指定转发目标,就会调用methodSignatureForSelector方法,用来返回一个方法签名,这也是跳转方法的最后机会。

如果methodSignatureForSelector方法返回正确的方法签名就会调用forwardInvocation方法,forwardInvocation方法内提供一个NSInvocation类型的参数,NSInvocation封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数。在forwardInvocation函数内修改方法调用对象即可。

如果methodSignatureForSelector返回的为nil,就会来到doseNotRecognizeSelector:方法内部,程序crash报出经典的错误unrecognized selector sent to instance

消息转发.png

至此,OC消息机制的分析就告一段落,OC中的方法调用其实都是转成了objc_msgSend函数的调用,给方法调用者(receiver)发送一条消息(selector方法名)。方法调用过程包括三个阶段:消息发送、动态方法解析、消息转发。

更多技术知识请关注公众号
iOS进阶


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

推荐阅读更多精彩内容