「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,定位到 bucketsindex。
  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函数

resolveInstanceMethod和resolveClassMethod也称为方法的动态决议。

resolveInstanceMethod方法

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

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

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

lookUpImpOrNilTryCache方法

在resolveInstanceMethod和resolveClassMethod中都会调用的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 对象,它封装了以下信息:

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

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

  • i 表示 int
  • @ 表示 id
  • : 表示 SEL
  • v 表示 void
相关推荐
茶底世界之下1 小时前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift
传奇开心果编程4 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程10 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink12 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程13 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架15 小时前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer