【OC底层_消息发送与转发机制原理】

前言

OC一直以来被称为动态语言,Objective-C 是一门动态的语言,在学习KVO和KVC的时候就能理解一些了,当然不单单是因为这个特质,OC还具有动态绑定动态类型等特点,接下来学习的消息传递和消息转发机制就能深刻的体会这些特别的动态特点。

简单理解一下动态绑定和动态类型的意思

动态类型

动态类型:Objective-C 中的对象是动态类型的,这意味着你可以在运行时动态地发送消息给对象,并且对象可以根据接收到的消息来执行相应的方法。这与静态类型语言不同,静态类型语言在编译时需要明确指定方法的调用。

动态绑定

动态绑定:Objective-C 使用动态绑定来实现方法调度。在运行时,Objective-C 运行时系统会根据消息的接收者来确定要调用的方法。这意味着你可以在运行时决定调用哪个方法,而不需要在编译时就确定。

为何为动态语言?

Objective-C是一门动态的语言,以至于确定调用哪个方法被推迟到了运行时,而非编译时。与之相反,C语言使用静态绑定,也就是说在编译期就能决定程序运行时所应该调用的函数,所以在C语言中, 如果某个函数没有实现,编译时是不能通过的。而Objective-C是相对动态的语言,运行时还可以向类中动态添加方法,所以编译时并不能确定方法到底有没有对应的实现,编译器在编译期间也就不能报错。

  • 例如一个按钮,并没有实现他的响应事件会警告但不会报错,当我在运行的时候点击就会报错。这就是动态的体现,当然这也是不安全的体现,在swift里面就没有这样子。
    请添加图片描述

Swift

请添加图片描述

  • 在swift里面没有写相应的方法的实现会直接会报错,swift被称为极度安全的语言。

什么是消息传递

当在对象上调用方法在Objective-C里面非常普遍,这个就叫给某个对象发送消息,消息有名称或者选择子这个说法,消息也不单单可以接受参数,还可以有返回值。

在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用那个方法则完全运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。

给某个对象发送消息

id returnValue = [someObject messageName:parameter];
  • Objective-C 中,[someObject messageName:parameter] 是发送消息的语法。这条语句表示向 someObject 对象发送名为 messageName 的消息,并传递参数 parameter
  • 由于 Objective-C 是一门动态的语言,编译器在编译时无法确定消息的接收者 someObject 的具体类型和消息的具体实现。因此,需要在运行时通过 Objective-C 运行时系统来解析消息并执行相应的方法。在运行时,编译器会把上面这个格式的方法调用转化为一条标准的C语言函数调用实现:objc_ msgSend()该函数是消息objc里在运行时传递机制中的核心函数。
void objc_msgSend(id self, SEL cmd, ....
  • objc_msgSend() 函数的作用是将消息发送给对象,并执行相应的方法。它接受多个参数,其中包括消息的接收者对象、要调用的方法的选择器以及任何方法的参数。根据选择器,运行时系统会找到接收者对象的方法实现,并在运行时动态调用该方法。

运行时,上面Objc的方法调用会被翻译成一条C语言的函数调用, 如下:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter)

选择子SEL

Objective-C 中,选择子(Selector)是用于表示方法名的数据类型。它是一个在运行时由编译器生成的唯一的标识符,用于在对象中查找并调用相应的方法。

OC在编译时会根据方法的名字(包括参数序列),生成一个用来区分这个办法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么他们的ID就是相同的。所以不管是父类还是子类,名字相同那么ID就是一样的

请添加图片描述

Runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。

IMP

什么是IMP?

IMP:一个函数指针,保存了方法地址

  • 它是OC方法实现代码块的地址,通过他可以直接访问任意一个方法,免去发送消息的代码。

IMP是”implementation”的缩写,它是objetive-C 方法(method)实现代码块的地址,可像C函数一样直接调用。通常情况下我们是通过[object method:parameter]或objc_msgSend()的方式向对象发送消息然后Objective-C运行时(Objective-C runtime)寻找匹配此消息的IMP,然后调用它;但有些时候我们希望获取到IMP进行直接调用。

声明一个IMP:

typedef id (&IMP)(id,SEL,...);

IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id.

IMP和SEL的关系

  • SEL是方法的指针,类似于一种编号。
  • IMP是函数指针,保存了方法的地址。
    在 Objective-C 中,SELIMP 是用于表示方法的选择子和方法的实现的类型。
  • SEL(选择子):
    • SEL 是一个代表方法名的数据类型,它由编译器在编译时生成。
    • SEL 是一个唯一的标识符,用于在运行时查找和调用相应的方法。
    • 使用 @selector 关键字可以将方法名转换为 SEL 类型。
    • SEL 主要用于动态调用方法和在运行时检查、比较方法名。
  • IMP(方法实现):
    • IMP 是一个函数指针类型,用于表示方法的实现代码的内存地址。
    • IMP 指向方法的实际代码,其中包含了方法的具体实现逻辑。
    • IMP 由编译器在编译时生成,并与 SEL 相对应。
    • 在运行时,通过 SEL 可以找到相应的 IMP,从而调用方法的实现。

联系:

  • SEL 和 IMP 是密切相关的,它们共同用于实现 Objective-C 的动态特性。
  • SEL 用于标识方法名,而 IMP 则表示方法的具体实现。
  • 在 Objective-C 运行时系统中,通过 SEL 可以查找并关联对应的 IMP,从而在运行时动态调用方法。
  • SEL 和 IMP 的配合使用使得 Objective-C 可以在运行时实现消息的动态派发和方法的动态调用。

总结: SEL 是方法名的唯一标识符,用于查找方法的实现,而 IMP指向方法实现代码的函数指针。它们共同构成了 Objective-C 运行时系统中的方法调用机制,实现了 Objective-C 的动态特性。

消息发送

OC是一门动态的语言,在编译的时候不知道具体类型,运行的时候才会检查数据类型,根据函数名找到函数的实现,实现语言动态的就是runtime。核心有二:

  1. 动态配置:动态的修改类的信息,添加属性方法甚至成员变量的值等数据结构。
  2. 消息传递:包括发送和转发,编译时期方法调用转换成objc_msgsend函数进行消息的发送,通过SEL查找IMP的实现过程(方法名查找方法实现)

1. 快速查找

objc_msgSend()的执行流程

  1. 消息发送:负责从类及父类的缓存列表及方法列表查找方法
  2. 动态解析:如果消息发送阶段没有找到方法,则会进入动态解析阶段:负责动态地添加方法实现
  3. 消息转发:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理

消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

objc_msgSend()深基

作为发送消息的核心主人公,他的逻辑如下

id objc_msgSend(id self, SEL _cmd, ...) {
    
    
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}

核心逻辑及其参数解析

  • 核心逻辑
    这是 objc_msgSend() 函数的一个简化版本的示例实现。该函数用于在 Objective-C 运行时发送消息并调用方法。
  1. 获取 self 对象的类(Class):使用 object_getClass(self) 函数获取 self 对象的类对象。这个类对象用于在运行时获取方法的实现。
  2. 获取方法的实现(IMP):使用 class_getMethodImplementation() 函数根据类对象和选择器 _cmd 获取对应方法的实现。这个函数返回一个指向方法实现的函数指针(IMP)。
  3. 调用方法的实现:如果获取到了有效的方法实现(非空),则通过函数指针 imp 调用该方法的实现。使用 imp() 函数并传递 self_cmd 和其他可能的参数进行方法调用。
  4. 返回结果:返回方法的返回值。

这只是 objc_msgSend() 函数的一个简化版本示例,实际的 objc_msgSend() 函数会处理更多的细节,包括消息转发、方法缓存等。

  • 参数解析
    objc_msgSend在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。如果方法本身有参数,会把本身的参数拼接到这两个参数后面。

源码解析

objc_msgSend()源码是用汇编语言写的,它针对不同的架构实现也不同,汇编语言的效率比c/c++更快,它直接对寄存器进行访问和操作,相比较内存的操作更加底层效率更高。

Apple在汇编方法的命名之前都是以下划线开头的,防止了符号的冲突。

看看函数的核心部分代码


		//进入objc_msgSend流程
	ENTRY _objc_msgSend
    //流程开始,无需frame
	UNWIND _objc_msgSend, NoFrame

    //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
	cmp	p0, #0			// nil check and tagged pointer check
//如果支持小对象类型,返回小对象或空
#if SUPPORT_TAGGED_POINTERS
    //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
    //等于,如果不支持小对象,就跳转至LReturnZero退出
	b.eq	LReturnZero
#endif
    //通过p13取isa
	ldr	p13, [x0]		// p13 = isa
    //通过isa取class并保存到p16寄存器中
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class


该段代码是 是objc_msgSend 开始到找类对像cache方法结束的流程。其中还有if else的原型

首先判断receiver是否存在,以及是否是taggedPointer类型的指针,如果不是taggedPointer类型,我们就取出对象的isa指针(x13寄存器中),通过isa指针找到类对象(x16寄存器),然后通过CacheLookup,在类对象的cache中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached分支。

消息发送的快速查找总结:objc_msgSend(receiver, sel, …)

  • 检查消息接收者receiver是否存在,为nil则不做任何处理
  • 通过receiver的isa指针找到对应的class类对象
  • 找到class类对象进行内存平移,找到cache
  • cache中获取buckets
  • buckets中对比参数sel,看在缓存里有没有同名方法
  • 如果buckets中有对应的sel --> cacheHit --> 调用imp
  • 如果在缓存中没有找到匹配的方法选择子,则执行慢速查找过程,即调用 _objc_msgSend_uncached 函数,并进一步调用 _lookUpImpOrForward 函数进行全局的方法查找。

总的来说,消息发送会先通过缓存快速查找方法实现如果缓存中没有找到,则进入慢速查找过程,从类的方法列表、父类链等逐级查找,直到找到匹配的方法实现或者最终抛出异常。

对于buckets理解

这里需要说明一下buckets:buckets 是缓存(cache)中的一部分。

  • 在 Objective-C 的消息发送过程中,每个类对象都有一个缓存结构,用于存储最常用的方法实现。缓存结构中包含多个 bucket,每个 bucket 可以存储一个方法选择子(SEL)及其对应的方法实现(IMP)。
  • 进行消息发送时,系统会首先检查缓存中是否存在与目标方法选择子匹配的方法实现。这个检查是通过遍历缓存中的 bucket,逐个对比方法选择子来实现的。如果找到匹配的方法选择子,就可以直接调用相应的方法实现,从而避免了慢速查找的过程。
  • buckets 可以看作是缓存中存放方法选择子和对应方法实现的槽位。具体的实现方式可能因不同的编译器和平台而有所不同,但它们的目标都是提供一种快速查找的机制,避免每次消息发送都进行完整的查找过程。

方法缓存

简单理解一下缓存的概念,上面的总结都提到了缓存这个概念

如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率

2. 慢速查找

什么时候用到 lookUpImpOrForward

_lookUpImpOrForward 是 Objective-C 运行时中的一个函数,用于在消息发送过程中进行慢速查找或转发操作。

当消息发送的快速查找过程无法找到匹配的方法实现时,就会进入 _lookUpImpOrForward 函数。这个函数的主要功能是根据给定的接收者对象、方法选择子,以及其他相关信息,在运行时中查找适当的方法实现,并进行后续处理。

_lookUpImpOrForward 函数的具体行为取决于对象所处的状态和环境。它可能执行以下操作之一:

  1. 慢速查找(Slow Path Lookup):如果对象所属的类没有提供指定方法选择子的实现,那么会根据继承链和协议信息逐级查找,直到找到匹配的方法实现,或者最终抛出异常。
  2. 消息转发(Message Forwarding):如果经过慢速查找仍然无法找到匹配的方法实现,那么就会触发消息转发机制。在消息转发过程中,运行时系统会给对象机会处理未知的方法调用,可以动态添加方法实现、替换为其他对象来处理方法调用,或者抛出异常。

_lookUpImpOrForward 函数是 Objective-C 运行时中非常重要的一环,它确保在消息发送过程中能够处理各种情况,保证对象能够正确地响应方法调用。具体的实现细节可能因编译器和平台而有所不同,但其核心目标是在运行时中查找适当的方法实现或触发消息转发机制。

lookUpImpOrForward 深基

方法实现简单看看

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().

    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel); //cache中找IMP
            if (imp) goto done_unlock; //找到就直接返回了
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
     //如果不是常量优化缓存
            // 当前类的方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        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.
            break;
        }
        if (fastpath(imp)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

    // 没有实现,尝试一次方法解析器。
	// 这里就是消息转发机制第一层的入口
    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 // iOS操作系统且真机的情况下
        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;
}

该代码是Objective-C 运行时中的 _lookUpImpOrForward 函数的部分实现。这个函数在消息发送过程中进行慢速查找或转发操作。

首先,代码检查接收者 inst 是否为空,如果为空则直接返回空。

接下来,代码根据接收者的类对象 cls 进行一系列的处理和查找操作,以找到适当的方法实现 imp。这包括:

  1. 检查类对象是否已经初始化,如果尚未初始化则将 LOOKUP_NOCACHE 标志添加到 behavior 中,避免缓存查找。
  2. 通过 realizeAndInitializeIfNeeded_locked 函数对类对象进行实例化和初始化处理,确保类对象已经准备就绪。
  3. 使用循环逐级查找方法实现,包括在类的缓存中查找、在类的方法列表中查找、在父类链中查找。如果找到了匹配的方法实现,则跳转到 done 标签处。
  4. 如果在查找过程中找不到匹配的方法实现,则说明需要进行消息转发。将消息转发的默认实现 forward_imp 赋给 imp
  5. 如果设置了 LOOKUP_RESOLVER 标志,说明需要调用方法解析器进行进一步处理,跳转到 resolveMethod_locked 函数进行解析。
  6. 在查找或转发结束后,如果未设置 LOOKUP_NOCACHE 标志,将找到的方法实现 imp 缓存到类对象的缓存中。

最后,代码解锁运行时锁,根据需要返回找到的方法实现 imp 或空值。

_lookUpImpOrForward的核心

// unreasonableClassCount()表示循环的上限;
    for (unsigned attempts = unreasonableClassCount();;) {
    
    
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    
    
            // 如果是常量优化缓存
            // 再一次从cache查找imp
            // 目的:防止多线程操作时,刚好调用函数,此时缓存进来了
#if CONFIG_USE_PREOPT_CACHES // iOS操作系统且真机的情况下
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
    
    
            // curClass方法列表。
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
    
    
                imp = meth->imp(false);
                goto done;
            }
            // 每次判断都会把curClass的父类赋值给curClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    
    
                // 没有找到实现,方法解析器没有帮助。
                // 使用转发。
                imp = forward_imp;
                break;
            }
        }

        // 如果超类链中存在循环,则停止。
        if (slowpath(--attempts == 0)) {
    
    
            _objc_fatal("Memory corruption in class list.");
        }

        // 超类缓存。
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
    
    
            // 在超类中找到forward::条目。
            // 停止搜索,但不要缓存;调用方法
            // 首先为这个类解析器。
            break;
        }
        if (fastpath(imp)) {
    
    
            // 在超类中找到方法。在这个类中缓存它。
            goto done;
        }
    }

上述代码段是 _lookUpImpOrForward 函数中的一部分,用于在类对象的继承链中进行方法查找

代码首先使用 unreasonableClassCount() 函数确定了循环的上限次数。然后进入一个无限循环。

在循环中,首先检查当前类对象的缓存是否是常量优化缓存(isConstantOptimizedCache)。如果是常量优化缓存,代码尝试从缓存中获取方法实现(cache_getImp(curClass, sel))。如果成功获取到方法实现,则跳转到 done_unlock 标签处,结束查找。

如果当前类对象的缓存不是常量优化缓存,代码继续执行。通过调用 getMethodNoSuper_nolock 函数在当前类对象的方法列表中查找方法(meth = getMethodNoSuper_nolock(curClass, sel))。如果找到匹配的方法,则获取对应的方法实现(imp = meth->imp(false)),跳转到 done 标签处,结束查找。

如果在当前类对象的方法列表中没有找到匹配的方法实现,代码继续执行。将当前类对象的父类赋值给 curClass,并判断是否为 nil。如果父类为 nil,说明已经到达了继承链的顶端,没有找到匹配的方法实现。此时将默认的转发实现 forward_imp 赋给 imp,并跳出循环。

在循环的每次迭代中,会将 attempts 的值减一,表示尚未完成的查找次数。如果 attempts 的值减到零,则说明类对象的继承链中存在循环,这是不合理的。此时会触发一个错误,终止程序执行。

如果在当前类对象的缓存中找到了转发的条目(imp == forward_imp),表示在父类的缓存中找到了转发的方法实现。这时会停止循环,但不会将转发的方法实现缓存,而是先调用方法解析器来处理。

最后,在循环结束后,会根据需要将找到的方法实现缓存到类对象的缓存中,然后解锁运行时锁,并根据需要返回找到的方法实现或空值。

该代码段展示了在类对象的继承链中进行方法查找的过程。它首先尝试从缓存中获取方法实现,然后逐级向上查找,直到找到匹配的方法实现或到达继承链的顶端。如果找不到匹配的方法实现,则采用转发的方式处理。

慢速查找IMP总结 IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

  1. 从本类的 method list (二分查找/遍历查找)查找imp
  2. 从本类的父类的cache查找imp(汇编)
  3. 从本类的父类的method list (二分查找/遍历查找)查找imp …继承链遍历…(父类->…->根父类)里找cache和method list的imp
  4. 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
  5. 直到查找到nil,指定imp为消息转发,跳出循环,执行动态决议resolveMethod_locked(消息转发的内容)
    在这里插入图片描述

消息转发

在消息发送之后还是没有找到方法,我们会进行消息转发机制,但是在这之前,我们还会进行一个步骤,叫动态决议

动态决议 resolveMethod_locked

当本类和本类继承链下的cachemethod list都查找不到impimp被赋值成了_objc_msgForward_impcache但是它没有调用,会进入动态方法解析流程,并且只会执行一次。

resolveMethod_locked 是 Objective-C 运行时中的方法解析器的实现之一。它用于解决在消息发送过程中找不到方法实现的情况。下面是 resolveMethod_locked 函数的代码:

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    
    
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //判断是不是元类
    if (! cls->isMetaClass()) {
    
    
        // 不是元类,则是实例方法的动态方法解析
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
    
    
        // 是元类,则是类方法的动态方法解析
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls); // inst:类对象   cls: 元类
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    
    
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

上述代码流程如下:

  1. 判断进行解析的是否是元类
  2. 如果不是元类,则调用_class_resolveInstanceMethod进行对象方法动态解析
  3. a. 如果是元类,则调用_class_resolveClassMethod进行类方法动态解析
    b. 完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析

resolveInstanceMethodresolveClassMethod。也称为方法的动态决议。

lookUpImpOrForwardTryCache

上述执行resolveMethod_locked方法后返回lookUpImpOrForwardTryCache请添加图片描述
本质上调用了_lookUpImpTryCache方法

_lookUpImpTryCache里面发现可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImpcache里找一遍方法的sel请添加图片描述
如果(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。
请添加图片描述
第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时

// 这里就是消息转发机制第一层的入口
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    
    
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

根据变化后的behavior值和LOOKUP_RESOLVER值之间的关系导致该if语句内部只能进入第一次,因此这个判断相当于单例。解释了为什么开头说的该动态解析resolveMethod_locked为什么只执行一次。

进入消息转发

如果系统在动态决议阶段没有找到实现,就会进入消息转发阶段。分别是消息的快速转发和消息的慢速转发。

cache没有找到imp,类的继承链里的方法列表都没有找到imp,并且resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。

1. 快速转发(消息接受者替换)

转发的作用在于,如果当前对象无法响应消息,就将它转发给能响应的对象。

当一个对象无法响应某个特定的方法时Objective-C运行时会自动调用该对象的forwardingTargetForSelector:方法,给开发者一个机会返回一个能够响应该方法的对象。该方法的签名如下:

- (id)forwardingTargetForSelector:(SEL)aSelector;

开发者可以在该方法中根据需要返回一个实现了该方法的对象,使得该对象能够接收并处理该消息。返回的对象会被用于接收消息,并执行对应的方法。如果返回nil,则进入下一步的消息转发机制。

快速转发也是消息接受者替换,所以如果本类没有能力去处理这个消息,那么就转发给其他的类,让其他类去处理。

//第二根稻草,使用快速消息转发,找其他对象来实现方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    
    if (aSelector == @selector(methodTwo)) {
    
    
        //也就是本类中的其他对象,此处选取的对象是forwardObject
        return self.forwardObject;
    }
    return nil;
}

2. 慢速转发(完整消息转发)

如果forwardingTargetForSelector方法返回的是nil,那么我们还有最后一根稻草可以抓住完全消息转发。相比于快速转发,不仅可以替换消息接受者,还能替换方法:

//第三根稻草,使用完全消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    
    if (aSelector == @selector(methodThree)) {
    
    
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sig;
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    
    //选择一个函数去替换
    anInvocation.selector = @selector(methodNormal);
    //选择一个消息接收者(对象)去替换
    anInvocation.target = self.forwardObject;
    [anInvocation invoke];
}

这里有两个类,NSMethodSignatureNSInvocation。其中NSMethodSignature是方法签名,可以通过方法的字符来实例化。NSInvocation是方法调用实体,其中有targetselector和参数构成。

这第三个救命稻草的逻辑就是: 先判断methodSignatureForSelector有没有被实现且返回值不为nil,如果已经实现且返回值不为nil,那么就进行下一步判断forwardInvocation有没有被实现,如果forwardInvocation已经实现那么就使用方法签名生成NSInvocation对象并调用forwardInvocation方法,最后返回forwardInvocation执行的结果,如果forwardInvocation方法没有被实现,那就直接调用doesNotRecognizeSelector 方法打印日志抛出异常。如果methodSignatureForSelector没有被实现或返回值为nil,那么就直接调用doesNotRecognizeSelector 方法打印日志抛出异常。

需要注意的是,forwardInvocation:方法在methodSignatureForSelector:方法返回非nil的方法签名时才会被调用。如果methodSignatureForSelector:返回了nil,那么forwardInvocation:将不会被触发,而是进入消息转发的最后一步:动态方法解析或抛出异常。

消息转发的三次拯救
请添加图片描述

总结

消息传递

  1. OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程

动态决议

  • 通过消息发送机制也找不到方法,系统在进入消息转发前,还会进行动态决议。(分为类方法动态决议和实例方法动态决议)

消息转发

  • 动态决议也找不到方法,才真正进入消息转发环节。
  • 动态决议、快速转发(接受者替换)、慢速转发(完整消息转发)合称为三个救命稻草,用于防止方法查找导致的系统崩溃。

快速:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    

慢速:

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 正向调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;

消息转发机制基本上分为三个步骤,也被称为消息的三次拯救:

  1. 动态方法解析 : 调用resolveInstanceMethod或者resolveClassMethod方法,尝试给一个没有实现的方法添加实现(动态解析)
  2. 备援接收者: 调用forwardingTargetForSelector尝试让本类中其他的对象去执行这个函数(快速地消息转发)
  3. 完整消息转发: 如果没有进行快速转发,可以用methodSignatureForSeletorfrowardInvocation 来进行完全消息转发,不仅可以替换消息接受者,还能替换方法。(完整的消息转发)

大佬的流程图两张总结附上:
消息发送和转发机制原理
在这里插入图片描述

在这里插入图片描述

  • runtime通过selector找到对应的IMP实现:缓存查找-当前类查找-父类逐级查找。
  • 子类调用父类的方法,缓存在当前传入的类

寒假的学习还是比较浅的,因为没有接触到OC的类和对象的底层,慢慢学习了元类父类等关系和一些底层的原理,看消息发送和转发机制就比较轻松了。

猜你喜欢

转载自blog.csdn.net/weixin_61639290/article/details/131299200