「iOS」————消息传递和消息转发

UI学习


消息传递和消息转发

  • SEL就像是方法的 "名字",是一个字符串,用于在运行时查找方法。
  • IMP是方法的具体实现,是一个函数指针。
  • _cmd是方法内部的一个参数,代表当前正在执行的方法选择器。

选择子SEL

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

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

IMP

IMP是一个函数指针,它指向方法的实际实现。当运行时系统找到了与选择器相匹配的方法时,它会获取该方法的IMP,然后调用这个函数指针来执行方法的代码。

IMP通常被声明为id返回类型和接受id类型的self和SEL类型的_cmd参数的函数指针。

一般通过SEL来查找方法的IMP

快速查找是针对缓存,慢速查找是查找本类的方法列表和继承链的缓存和方法列表

简短总结:

  • 【快速查找流程】首先,在类的缓存cache中查找指定方法的实现
  • 【慢速查找流程】如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找
  • 【动态方法决议】如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法
  • 【消息转发】如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发
  • 如果转发之后也没有,则程序直接报错崩溃unrecognized selector sent to instance

消息传递的流程

  • 首先,RunTime通过obj的isa指针找到其所属的class
  • 接着在这个类的缓存中查找与选择器匹配的方法实现。(即快速查找)
  • 如果缓存中没找到,那就在这个类的方法列表中查找与SEL匹配的IMP。
  • 如果当前类没找到,Runtime会沿着类的继承链往他的superclass中查找,先查找缓存,在查找方法列表,直到根类。
  • 一旦找到这个函数,就会执行他的IMP
  • 如果知道根类都没有找到,则会进行消息转发流程。

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

快速查找是针对缓存,慢速查找是查找本类的方法列表和继承链的缓存和方法列表

消息传递

快速查找

  1. 首先判断 receiver 是否为 nil 或 tagged pointer,若是则直接返回。
  2. 通过 receiver 的 isa 指针获取类对象,再通过类对象结构体偏移找到 cache_t 结构。
  3. cache_t 结构中包含 buckets(方法缓存表)和 mask(哈希掩码)。
  4. 用 cmd 的哈希值与 mask 做与运算,得到索引 index,定位到 buckets[index]。
  5. 检查 bucket 中的 sel 是否等于 cmd,若相等则命中(hit),返回 imp。
  6. 若不等,则采用线性探测法(index++,循环查找),直到找到空 bucket 或回到起始位置。
  7. 若遍历一圈未找到,则进入 jumpmiss,走慢速查找流程。

慢速查找 :进入 _lookUpImpOrForward 函数

  1. 若 cache 未命中,则进入 lookUpImpOrForward。
  2. 检查 cls 是否为有效类对象,否则报错。
  3. 若类未初始化,则先初始化。
  4. 遍历当前类的方法列表查找 cmd,若找到则返回 imp,并写入 cache。
  5. 若未找到,则递归查找父类,直到父类为 nil。
  6. 若最终未找到 imp,则尝试动态方法解析(resolveInstanceMethod)。
  7. 若解析失败,则进入消息转发流程(objc_msgForward)。
  8. 查找过程中每次命中 imp 都会写入 cache 以优化下次查找。

动态决议 resolveMethod_locked

慢速查找没找到IMP时,进入方法动态解析。

  • 首先判断进行解析的是否是元类

  • 如果不是元类,则调用_class_resolveInstanceMethod进行实例方法动态解析

  • 如果是元类,则调用用_class_resolveClassMethod进进行类方法动态解析如过没有找到,则在调用_class_resolveInstanceMethod找实例方法,因为类是元类的实例。

  • 当完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析

  • 最后执行 lookUpImpOrForwardTryCache函数

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

resolveInstanceMethod方法

  • 针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,我们有一次挽救的机会,即尝试一次动态方法决议,由于是实例方法,所以会走到resolveInstanceMethod方法
  • 发送resolveInstanceMethod消息前,需要查找cls中是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    • 如果没有,则直接返回
    • 如果有,则发送resolveInstanceMethod消息
  • 再次慢速查找实例方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找实例方法

注意:此处进入慢速查找中,是查找实例方法动态解析是否实现。

之后调用lookUpImpOrNilTryCache方法,重新返回到方法查找的流程当中去;

lookUpImpOrNilTryCache方法

resolveInstanceMethodresolveClassMethod中都会调用的lookUpImpOrNilTryCache方法

objective-c 复制代码
extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0);

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    //**这里behavior没传,所以是默认值0**
    //**behavior | LOOKUP_NIL = 0 | 4 = 4**
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

给参数behavior拼接上LOOKUP_NIL然后调用_lookUpImpTryCache方法

lookUpImpTryCache方法

objectivec 复制代码
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    //**判断类是否初始化**
    if (slowpath(!cls->isInitialized())) {
        //**没有初始化直接调用lookUpImpOrForward**
        //**里面针对没初始化的类,有相关处理**
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    //**去缓存中,查找sel是否有对应的imp**
    IMP imp = cache_getImp(cls, sel);
    //**找到了则跳去done**
    if (imp != NULL) goto done;
    //**没找到继续往下走,去共享缓存中查找**
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    //**没找到对应的imp,调用lookUpImpOrForward方法查找,behavior = 4**
    //** 4 & 2 = 0 ,所以这次方法查找不会再次进行动态方法决议**
    //**将_objc_msgForward_impcache缓存起来,方便下次直接返回**
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    //**命中缓存中,并且sel对应的imp为_objc_msgForward_impcache**
    //**说明动态方法决议已经执行过,且没有添加imp,则直接返回空**
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    //**说明动态方法决议中添加了对应的imp**
    return imp;
}
  • 首先判断类是否初始化,如果没有初始化则直接调用lookUpImpOrForward,里面有针对没初始化的类进行相应的处理;

  • 然后去缓存中进行方法的快速查找,找到了就去done

  • 缓存中没找到,如果支持共享缓存,则去共享缓存中查找

  • 都没有查找到,则通过慢速方法查找去查找方法,由于behavior 的值发生改变,这次慢速查找不会再次调用动态方法决议

  • 在done流程中,如果已经执行过动态方法决议且并没有添加imp,则缓存中的sel对应imp为_objc_msgForward_impcache,这时直接返回nil。否则返回添加的imp实现。

以上则是这个歌动态决议阶段,如果都没找到实现,则进入消息转发流程:

消息转发

消息转发的方法有三个:

  • 【快速转发】forwardingTargetForSelector
  • 【慢速转发】
    • methodSignatureForSelector
    • forwardInvocation

方法签名

方法签名本质上是一个 NSMethodSignature 对象,它封装了以下信息:

  • 返回值类型 :例如 intNSString*void
  • 参数数量
  • 每个参数的类型
  • 方法调用的调用约定(Calling Convention)

在底层,这些类型信息通常使用 Type Encodings 来表示,这是一种将类型信息编码为字符串的方式。例如:

  • i 表示 int
  • @ 表示 id
  • : 表示 SEL
  • v 表示 void
相关推荐
谈吐大方的鹏sir36 分钟前
SwiftUI-Text组件学习
ios
不自律的笨鸟2 小时前
iOS 26,双版本更新来了
ios·iphone
他们都不看好你,偏偏你最不争气18 小时前
iOS —— 天气预报仿写总结
ios
白玉cfc1 天前
【iOS】网易云仿写
ui·ios·objective-c
归辞...1 天前
「iOS」——内存五大分区
macos·ios·cocoa
HX4361 天前
MP - List (not just list)
android·ios·全栈
忆江南1 天前
NSProxy是啥,用来干嘛的
ios
忆江南1 天前
dyld
ios
归辞...2 天前
「iOS」——GCD其他方法详解
macos·ios·cocoa