objc_msgSend消息发送机制

一、消息发送

我们先来了解一下什么是消息发送;C语言是静态,OC是动态类型。在编译的时候不知道具体类型,运行的时候才会检查数据类型,根据函数名找到实现。实现语言动态的就是Runtime的API,主要有两大核心:

  • 动态配置:动态的修改类的信息。添加属性、方法、甚至成员变量的值等数据结构。
  • 消息传递:包括发送和转发。在编译的时候,方法调用会转化为objc_msgSend函数进行消息发送,即通过sel(方法名)找imp(方法实现)的过程。

二、objc_msgSend

我们通过一个demo来开始探索:

#import <Cocoa/Cocoa.h>
#import <objc/message.h>

@interface LGPerson : NSObject

- (void)study;
- (void)happy;
+ (void)eat;

@end

@implementation LGPerson

- (void)study:(NSString *)arg {
    NSLog(@"%s",__func__);
}
- (void)happy {
    NSLog(@"%s",__func__);
}
+ (void)eat {
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGPerson *p = [LGPerson alloc];
        [p study:@"A"];
        [p happy];
        
    }
    return NSApplicationMain(argc, argv);
}

打开终端,在项目对应的目录下输入clang -rewrite-objc main.m将这个类转化为cpp文件,并找到其中关于main函数的部分,可以发现编译后的方法都是通过objc_msgSend发送的,这也证明了我们上面的想法,即方法的本质就是消息发送。

LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_64f0fb_mi_3);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));

同时,我们可以看到objc_msgSend带有默认的2个参数:消息的接收者id类型,消息的方法名SEl类型。alloc方法给类对象发消息,如果消息接收者是实例对象,那么实例对象就会通过isa指针找到类对象,从中找到实例方法。(类方法同理,在元类对象中找到)。如果方法带有参数,那么参数会跟在末尾。

此外,我们还在main.cpp文件中发现了objc_msgSend 家族,这些方法代表了发送给当前类对象,父类对象,等等。这也恰恰说明了为什么苹果要设计元类,就是为了objc_msgSend的复用。

三、objc_msgSendSuper

-(instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@",[self class]);
        NSLog(@"%@",[super class]);
    }
    return self;
}

我们对上面的代码进行编译发现:发送到对象超类的消息(使用super关键字)使用objc_msgSendSuper发送。接着我们从OC源码中看一下objc_msgSendSuper结构体的实现:结构体中的super_class等于父类,代表从父类对象开始查找。

我们来看一下objc_msgSend 和objc_msgSendSuper的区别

  • objc_msgSend的第一个参数是self(消息的接收者),第二个参数是消息的方法名字(sel)。objc_msgSendSuper的第一个参数是__rw_objc_super类型的结构体,结构体包含两个参数:第一个参数是self(消息的接收者),第二个参数是消息的方法名字(sel)
  • objc_msgSend是给本类发消息,objc_msgSendSuper是给父类发消息,结果相同,但出发点不同。所以在使用[self class] 和 [super class] 打印出来的都是self,即这个对象指向的类,因为消息的接收者不会发生改变。

四、快速查找

我们进入arm64下,看一下快速查找的过程,可以看到在其底层用到了汇编语言,这样的话可以直接使用参数,免去大量参数的拷贝开销。

五、慢速查找

我们在oc源码中查找_obcj_msgSend_uncached的入口,这是静态的STATIC_ENTRY。

进入MethodTableLookup内部:

然后,我们在进去_loopUpImpOrForward,因此我们来找一下loopUpImpOrForward:

在这个_loopUpImpOrForward,首先定义了一个消息的转发forWard_imp , 接着判断类的初始化、加锁、检查是否已知的类等,我们具体来看一下其中的for循环过程:

// unreasonableClassCount()表示循环的上限;
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.
          method_t *meth = getMethodNoSuper_nolock(curClass, sel);
          if (meth) {
              imp = meth->imp(false);
              goto done;
          }

          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.
          break;
      }
      if (fastpath(imp)) {
          // Found the method in a superclass. Cache it in this class.
          goto done;
      }
}

第一个if判断,是再次从从cache里找,防止多线程操作时,刚好调用函数,缓存进入,我们着重看一下else中的getMethodNoSuper_nolock:

接着跳转search_method_list_inline:

跳转findMethodInSortedMethodList:

接着跳转findMethodInSortedMethodList:

进行二分查找,其中probe--,退出时候得到列表里第一次出现的地方,这是意思就是分类优先,因为分类同名的方法会排在列表靠前。多个分类有同名方法时,确保后编译的先调用。

待查找完成后,会进入go done,并跳转log_and_fill_cache:

将方法插入到类的方法缓存中。

六、总结

1.方法的本质就是消息发送

消息的发送在编译的时候,编译器就会把方法转换为objc_msgSend这个函数。函数通过消息的接收者和方法名找到具体的实现。接收者是实例对象,通过isa找到类对象,再通过方法名在类对象的方法缓存中找到实现。如果接收者是类对象,就在元类里找。

2.objc_msgSendSuper 和 [super class] 在调用父类方法只是出发点不一样,但是结果是一样的。

使用super关键字调用父类方法,消息会通过objc_msgSendSuper发送。 superself调用方法的区别就在于,查找方法的时候出发点不一样。self会从当前类开始找,而super会从当前类的父类。

3.消息的快速查找流程

  • 判断receiver(消息的接受者)是否存在
  • receiver 通过isa 找到 class
  • class 首地址通过内存平移得到缓存cache
  • cache中获取buckets容器
  • 遍历buckets容器,与元素比对方法名(元素是bucket_t结构体类型),包含_sel和——imp成员变量
  • 如果找到相等的就执行CacheHit方法,调用imp
  • 如果没有,执行_objc_msgSend_uncached,进入慢速查找

4.消息的慢速查找流程

  • 开始lookUpImpOrForward,再次从cahe里查找,因为多线程可能已经缓存进来了
  • 先从当前类的methodList开始查找,已排序的用二分查找,未排序的用线性查找
  • 如果没有就找父类的cache
  • 再找父类的methodList
  • 如果父类为nil,就开始重复第二步直到父类均为nil为止
posted on 2022-05-09 17:00  suanningmeng98  阅读(560)  评论(0编辑  收藏  举报