【iOS】方法与消息底层分析

目录

前言

方法的本质

向不同对象发送消息

发送实例方法

发送类方法

[对象调用方法 实际执行是父类](#对象调用方法 实际执行是父类)

向父类发送类方法

消息查找流程

开始查找

快速查找流程

慢速查找流程

动态方法决议

应用场景

优化方案

消息转发机制

快速转发流程

应用场景

慢速转发流程

应用场景


前言

在OC底层中,方法的调用实质上是通过消息的发送实现的,这篇文章我们来看一看消息的发送是怎么样的

方法的本质

方法的本质就是通过objc_msgSend发送消息,有两个参数,第一个是id类型,表示消息接受者,第二个,表示方法编号。

向不同对象发送消息

发送实例方法

消息接收者是实例对象

发送类方法

本质上是向类对象发送消息

objc_getClass得到的是类对象

对象调用方法 实际执行是父类

Runtime中提供了一个接口处理这种情况:父类中实现了该方法,而子类没有实现该方法,子类对象调用方法,会执行父类中实现(符合继承的特性)

这个接口是objc_msgSendSuper,使用时还需要用到objc_super结构体,并给结构体赋值(receiver、super_class)

该结构体中receiver表示接收消息的实例对象,super_class表示父类类对象,根据这个赋值

可以看到这两种方式都是执行父类的实现,因此可以推断:方法调用首先在类中查找,如果找不到就到父类中查找

向父类发送类方法

上面向父类发送实例方法时,receiver表示实例对象,super_class表示父类类对象。而如果向父类发送类方法,reciever表示类对象,super_class表示父类元类对象

消息查找流程

消息查找的流程就是通过上层的sel发送消息objc_msgSend找到底层具体imp的实现的过程,objc_msgSend是用汇编写的而不是用C语言

开始查找

在开始objc_msgSend之后

  1. 首先会判断消息接受者是否为空,为空就直接返回

  2. 然后会判断是否为小对象,也就是是否为tagged_pointers

  3. 之后取对象中的isa存到寄存器p13中,根据isa进行mask地址偏移来得到对应的上级对象(类、元类)

取得了上级对象之后,就可以开始快速查找流程了,也就是在缓存中找imp的过程

快速查找流程

  1. 首先通过类的首地址偏移16字节找到cache的地址(cache离首地址16字节,isa占8字节,superclass占8字节),cache高16位存mask,低48位存buckets

  2. 然后从cache中分别取出buckets和mask,根据mask通过哈希算法算出哈希下标,根据哈希下标和bukets首地址来得到对应的bucket,bucket中存放着imp和sel

  3. 那么怎么确定找到的imp和sel就是要找的那个呢?主要是通过两层循环:

    1. 第一层循环:比较bucket中的sel和objc_msgSend中第二个参数_cmd是否相等:如果相等,就直接跳转到CacheHit,即缓存命中,返回imp;如果不相等,有三种情况:

      1. 一种是一直找不到,就直接跳转到CheckMiss,因为参数$0是normal,会跳转到__objc_msgSend_uncached,看英文就能明白意思就是没找到,这时就会进入慢速查找流程

      2. 第二种是如果获取到的bucket是第一个元素,那么就手动把它设置为最后一个元素,然后进行第二层循环

      3. 如果当前bucket不是第一个元素,那就继续当前的循环

    2. 第二层循环:和第一层循环基本相同,只是如果bucket还是等于buckets中第一个元素,就直接跳转到JumpMiss,此时也会跳转到没找到__objc_msgSend_uncached,进入慢速查找

慢速查找流程

慢速查找的过程分为汇编和C两个部分,这里我们不纠结汇编部分,汇编最后调用的是lookUpImpOrForward,这是一个C实现的函数

objectivec 复制代码
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // 定义的消息转发
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
​
    runtimeLock.assertUnlocked();
​
    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }
​
    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.
​
    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.
​
    //加锁,目的是保证读取的线程安全
    runtimeLock.lock();
​
    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类
    checkIsKnownClass(cls);
​
    //判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;
​
    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().
    
    //----查找类的缓存
    // unreasonableClassCount -- 表示类的迭代的上限
    //(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限的范围内,所以可以继续递归)
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            //---当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            //当前类 = 当前类的父类,并判断父类是否为nil
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                //--未找到方法实现,方法解析器也不行,使用转发
                imp = forward_imp;
                break;
            }
        }
​
        // Halt if there is a cycle in the superclass chain.
        // 如果父类链中存在循环,则停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
​
        // Superclass cache.
        // --父类缓存
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            // 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            //如果在父类中,找到了此方法,将其存储到cache中
            goto done;
        }
    }
​
    // No implementation found. Try method resolver once.
    //没有找到方法实现,尝试一次方法解析
​
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //动态方法决议的控制条件,表示流程只走一次
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
​
 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        //存储到缓存
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    //解锁
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

上面是慢速查找的源码,用自然语言来表述就是:

  1. 首先进行一次快速查找,也就是在cache缓存中查找,找到就直接返回imp,没找到就继续

  2. 先判断cls是否是已知类,如果不是就报错;再判断类是否实现,如果没实现需要先实现,这个时候实现的目的是为了确定它的父类链,ro以及rw等,方便之后数据读取和查找;还要判断是否初始化,没有就初始化

  3. 接下来进入for循环,沿着类或元类的继承链进行查找:

    1. 对于当前cls,在方法列表中使用二分查找进行查找,如果找到就进入cache写入流程并返回imp,如果没找到就返回nil

    2. 当前cls赋值为父类,如果父类为nil,imp = 消息转发,并终止递归,开始判断是否执行过动态方法解析

    3. 如果父类链中存在循环就报错

    4. 在父类中查找时,会先在父类缓存中查找,再在方法列表中查找

  4. 判断是否执行过动态方法解析,如果没有就执行动态方法解析,执行过一次的话就走消息转发流程

在二分查找过程中,如果找到的与key的value值相等,需要先排除分类方法

在进行完快速查找和慢速查找的流程之后,会进入动态方法决议和消息转发流程

动态方法决议

在查找流程没找到方法时,有一次机会补救就是动态方法决议,以实例方法为例,程序会走到resolveInstanceMethod方法:

用自然语言描述如下:

  1. 在发送resolveInstanceMethod消息前,先查找cls中有没有这个方法的实现,也就是通过lookUpImpOrNil方法进入lookUpImpOrForward慢速查找流程找这个方法:

    1. 如果没找到就直接返回

    2. 如果找到了就发送resolveInstanceMethod消息

  2. 再慢速查找实例方法的实现,又进行一次慢速查找

应用场景

使用动态方法决议可以解决一些方法未实现的报错,重写resolveInstanceMethod类方法并在其中将其指向其他方法的实现,比如有一个say666没实现,但是实现了sayMaster方法

类方法同理,将方法名改为resolveClassMethod即可

优化方案

在上面的场景中,我们需要对每一个类的方法进行重写,并且我们又知道慢速方法查找路径最后都会走到根类,因此我们可以为NSObjct添加分类来统一处理

消息转发机制

如果前面的过程都没找到该方法,那我也是没招了(bushi),那就会进行消息转发流程,消息转发流程分为快速转发和慢速转发,如果方法没有实现而崩溃报错,在崩溃之前会调用两遍动态方法决议,两遍快速转发,两遍慢速转发

快速转发流程

forwardingTargetForSelector在源码中只有声明,但是我们可以从帮助文档中看到有关于它的解释:

  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别的对象进行相关方法的处理,但是不能返回self,否则会一直找不到

  • 该方法的效率较高,如果不实现,会走到forwardInvocation:方法进行处理

  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送

  • 被转发消息的接受者参数、返回值等应和原方法相同

应用场景

比如TCJPerson没实现的方法,转发给实现了的TCJStudent

也可以直接调用父类的该方法,如果没找到的话会直接报错

慢速转发流程

methodSignatureForSelector慢速查找流程同样在帮助文档中寻找,可以发现forwardInvocationmethodSignatureForSelector必须同时存在

底层会通过方法签名生成一个NSInvocation,作为参数传递使用,接着查找可以响应NSInvocation中编码的消息的对象,找到后使用anInvocation将消息发送给该对象,并且anInvocation保存结果,运行时系统将提取结果并传递给原始发送者

应用场景

慢速转发的流程就是methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过NSInvocation来实现消息的转发

无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃

方法和消息的流程就到这里了,在上面的过程中你有没有注意到动态方法决议进行了两遍这个问题?它为什么会执行两遍呢?

其实第二次动态方法决议是在methodSignatureForSelectorforwardInvocation方法之间,是开始进行慢速消息转发之前再给的一次机会

相关推荐
Jackyzhe4 小时前
Flink学习笔记:整体架构
笔记·flink
徒 花4 小时前
初级网安作业笔记1
笔记
kfepiza4 小时前
Debian-10编译安装Mysql-5.7.44 笔记250706
linux·数据库·笔记·mysql·debian·bash
西西西仓鼠4 小时前
python学习打卡:DAY 40 训练和测试的规范写法
学习
xchenhao5 小时前
基于 Flutter 的开源文本 TTS 朗读器(支持 Windows/macOS/Android)
android·windows·flutter·macos·openai·tts·朗读器
今天背单词了吗9805 小时前
算法学习笔记:19.牛顿迭代法——从原理到实战,涵盖 LeetCode 与考研 408 例题
笔记·学习·算法·牛顿迭代法
DKPT6 小时前
Java设计模式之行为型模式(观察者模式)介绍与说明
java·笔记·学习·观察者模式·设计模式
future14127 小时前
C#进阶学习日记
数据结构·学习
lxsy8 小时前
spring-ai-alibaba 1.0.0.2 学习(十六)——多模态
人工智能·学习·ai-alibaba