本文主要内容

1、objc_msgSendSuper解析
2、办法的快速查找
3、办法的慢速查找算法
4、办法的慢速查找流程
5、总结

前言

在iOS底层原理之cache详解文章中,咱们了解到了cache,知道了insert操作,那么已然有存储的操作,就会有取的操作.在objc4-838.1源码cache源码中(62行)发现,编译器从cache中调用办法要用到objc_msgsendcache_getImp,如下图。音讯发送便是runtime经过sel找到imp的进程,音讯发送在编译阶段编译器会把这个进程转换成objc_msgSend函数。因而,接下来咱们来详细了解objc_msgSend这个函数。

iOS底层原理之方法调用的底层探究

扩展知识点
OC:为一门动态的言语,动态言语指程序在运转进程中,能够对类、变量进行修改,能够改动其数据结构,
比方能够添加或删去函数,能够改动变量的值等。在编译阶段并不知道变量的数据类型,也不清楚要调用哪些函数,
只有在运转时才去检查变量的数据类型,根据函数名找到函数的完成!
C:为一门静态的言语,在编译阶段就确定了所有变量的数据类型,同时也确定了要调用的函数,不能进行动态的修改!
runtime:便是能够完成言语动态的API。runtime的2个核心:一是类的各方面的动态装备;
二是音讯传递(音讯发送+音讯转发)。

一.objc_msgSendSuper解析

1、objc_msgSend初探

在之前咱们常常谈到办法调用便是音讯的发送,其本质便是objc_msgSend发送音讯。首先咱们新建一个工程,new一个LSPerson类,在这个类里边完成下面两个办法,代码如下:

@interface LSPerson : NSObject
- (void)study;
- (void)happy;
@end
// 在main办法调用
LSPerson *person = [LSPerson new];
[person study];
[person happy];

然后咱们命令行翻开main.m文件所在位置,履行clang -rewrite-objc main.m,编译main.m, 在同目录下得到main.cpp, 翻开这个文件找到main办法,咱们会找到如下代码:

// 对应的LSPerson *person = [LSPerson new];
LSPerson *person = ((LSPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LSPerson"), sel_registerName("new"));
// 对应的 [person study]
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("study
// 对应的 [person happy]
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("happy"));

经过上面的编译的代码咱们知道,调用办法实践最终便是调用objc_msgSend函数发送音讯,objc_msgSend函数默许有两个参数,第一个参数音讯接纳者,第二个参数便是音讯的办法名(sel), 假如办法有参数,就接着后边第三个、第四个等参数。objc_msgSend会根据音讯接纳者和音讯的办法名找到音讯的办法完成(imp)。假如音讯的接纳者是实例目标,那么就到实例目标的isa指针(类目标)中根据sel找到办法的完成(imp);假如音讯的接纳者是类目标,那么就到类目标的isa指针(元类)中根据sel找到办法的完成(imp);
这里有一个疑问,系统帮咱们编译成了objc_msgSend函数,那么咱们能否在代码中直接运用objc_msgSend函数了?答案是能够的。咱们将编译后的代码,直接替换上面的代码在code里边运转如下

// #import <objc/message.h> 需求引进系统的类库
LSPerson *person = [LSPerson new];
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("study"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("happy"));
//输出成果
// [LSPerson study]
// [LSPerson happy]

在上面编译的main.cpp里边咱们发现,不仅有objc_msgSend函数,还有objc_msgSendSuper等函数详细如下:

void objc_msgSend(void): 发送音讯
void objc_msgSendSuper(void): 发音讯给目标的父类时会编译成此函数
void objc_msgSend_stret(void): 发音讯的返回值为结构体时会编译成此函数 \

void objc_msgSendSuper_stret(void): 发音讯给目标的父类的返回值为结构体时会编译成此函数
void objc_msgSend_fpret(void): 发音讯的返回值为浮点型时会编译成此函数 接下来咱们看继续研讨objc_msgSendSuper函数。

2、objc_msgSendSuper函数

新添加一个类LSTeacher继承自LSPerson,在LSTeacher类里边重写init办法,添加NSlog办法,代码如下:

// @implementation LSTeacher
- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@", [self class]);
        NSLog(@"%@", [super class]);
    }
    return self;
}
- (void)study {
    [super study]
}
// mian.m
LSTeacher *teacher = [LSTeacher new];
[teacher study];
// 输出成果
// LSTeacher
// LSTeacher
// [LSPerson study]

从输出成果[self class] 输出LSTeacher,[super class]输出LSTeacher, [teacher study]输出的是 [LSPerson study];这里就有疑问了为什么 [super class]输出的是LSTeacher?.咱们将这个文件编译成Cpp看一下,履行clang -rewrite-objc LSTeacher.m得到LSTeacher.cpp文件,检查源码如下:

if (self = ((LSTeacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LSTeacher"))}, sel_registerName("init"))) {
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_xk_b2kwsh915s5bxyyjvtqt6dg40000gn_T_LSTeacher_4d907e_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_xk_b2kwsh915s5bxyyjvtqt6dg40000gn_T_LSTeacher_4d907e_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LSTeacher"))}, sel_registerName("class")));
}
  

[self class]: 在编译时会转化为objc_msgSend函数,所以输出LSTeacher
[super class]: 在编译时转化为objc_msgSendSuper函数,而该函数和objc_msgSend函数的仅有差异便是在找办法的时分,objc_msgSendSuper是先从父类中找办法的完成。同理[super study]打印出来的成果是先去父类中寻觅该办法的完成,所以打印出[LSPerson study];

为了更好的了解objc_msgSendSuper函数的调用机制,咱们手动完成super关键字,创立类型objc_super的结构体,先检查objc_super的类型界说如下

iOS底层原理之方法调用的底层探究
手动完成super关键字代码如下

// #import <objc/message.h>
- (void)study {
    NSLog(@"%s", __func__);
    struct objc_super ls_objc_super;
    ls_objc_super.receiver = self;
    ls_objc_super.super_class = LSPerson.class;
    void* (*objc_msgSendSuperTyped)(struct objc_super *self, SEL _cmd) = (void *)objc_msgSendSuper;
    objc_msgSendSuperTyped(&ls_objc_super, @selector(study));
}
// 输出成果
// [LSTeacher study]
解释:
1、当super_class 为父类时,就会去父类里边去查找办法
2、当把super_class 改成 LSTeacher.class 这个时分会造成无限循环调用该办法
3、当把super_class 改成 NSObject.class 这个时分程序会crash,因为NSObject里边并没有该办法的完成。

总结: objc_msgSendSuper函数objc_msgSend的函数仅有差异在于找办法的时分出发点不同,也便是、objc_msgSendSuper函数是从父类中找办法的完成

二、办法的快速查找

在上面咱们知道了objc_msgSend函数,咱们常常传闻调用办法的时分会有一个快速查找的流程,咱们结合objc4-838.1源码进行调试,在源码中查找objc_msgSend检查函数完成,这里咱们以arm64完成为主。代码部分截图如下

iOS底层原理之方法调用的底层探究
经过上面的代码咱们知道objc_msgSend底层是用汇编完成的。

扩展知识点
1、为什么objc_msgSend底层运用汇编? 主要原因是汇编比C快,同时汇编会免除很多对局部变量的复制效果,参数被直接存放在寄存器中。
2、汇编会在函数和全局变量前加一个下划线_ 在程序中往往会包含汇编和C文件,关于编译器来说两者是一样的,因而或许会出现问题,为了避免符号名的抵触在汇编中函数和全局变量前会加一个下划线啊_(如上面代码截图里边) 接下来咱们在咱们上面创立的工程里边添加断点经过汇编的方式来配合源码进行检查objc_msgSend的办法快速查找流程。

iOS底层原理之方法调用的底层探究
在上图的位置添加断点,当断点履行到这个地方的时分,开始汇编调试(Debug-> Debug Workflow -> Always show Disassembly)
iOS底层原理之方法调用的底层探究
按住Control,点击Step into单步调试
iOS底层原理之方法调用的底层探究
点击Step,进入objc_msgSend底层完成.
第1步 配合源码咱们知道先判断p0(音讯接纳者)是否存在,不存在则重新开始履行objc_msgSend。经过读取寄存器证明此刻的x0确实是LSTeacher目标.
iOS底层原理之方法调用的底层探究
第2步 经过p13读取isa(下图1为objc源码objc_msgSend底层函数完成),图二里边x13便是LSTeacher实例目标isa的地址
iOS底层原理之方法调用的底层探究
iOS底层原理之方法调用的底层探究
第3步 第二步取到目标isa地址之后,再经过isa取类目标,并保存到x16寄存器中,验证x16寄存器地址和LSTeacher类目标的地址相同,代码截图如下:
iOS底层原理之方法调用的底层探究
第4步 从x16中取出类目标移到x15中,经过x16内存平移得到cache 图1是objc源码objc_msgSend函数完成。
iOS底层原理之方法调用的底层探究
iOS底层原理之方法调用的底层探究
第5步 经过cache找存储办法的buckets。经过iOS底层原理之cache详解中经过源码分析cache的缓存内容的办法调试查找。
iOS底层原理之方法调用的底层探究
第6步 假如在cache中找到buckets,在buckets中找到对应的sel,就会调用cacheHit;假如没有找到,就会调用objc_msgSend_uncached函数。
iOS底层原理之方法调用的底层探究
iOS底层原理之方法调用的底层探究
总结:办法(实例办法和类办法)的快速查找
objc_msgSend(receiver, sel, …(其它办法的参数))\

  1. 判断 receiver是否存在;
  2. 经过receiverisa指针找到对应的class;
  3. class内存平移找到cache;
  4. 经过cache找到存储办法的buckets;
  5. 遍历buckets看缓存中是否存在sel办法;
  6. 假如buckets中缓存有sel办法,会调用cacheHit(缓存命中),然后会调用imp;
  7. 假如buckets中缓存没有sel办法,会调用objc_msgSend_uncached函数;

三、办法的慢速查找算法

在办法的快速查找中,假如buckets中缓存没有self办法,会调用objc_msgSend_uncached函数。本小节来分析此函数。找到objc_msgSend_uncached函数源码,履行MethodTableLookup,跳转lookUpImpOrForward函数。

iOS底层原理之方法调用的底层探究
iOS底层原理之方法调用的底层探究
lookUpImpOrForward函数完成中,如下部分是系统为调用此函数做的准备工作。
iOS底层原理之方法调用的底层探究
接着,再一次从cache里边取找imp,这是为了避免多线程操作时刚好调用办法,此刻缓存进来了。假如在cache还是找不到,就去办法列表中查找。
iOS底层原理之方法调用的底层探究
类的办法列表中查找详细查找代码如下:
iOS底层原理之方法调用的底层探究
iOS底层原理之方法调用的底层探究
运用二分法快速查找办法,成果或许找到也或许找不到,代码如下图
iOS底层原理之方法调用的底层探究
总结:当办法在cache里边找不到的时分会进入objc_msgSend_uncached函数,在这个函数里边会再次去cache里边找到(避免多线程问题),然后再去类目标办法列表中查找,查找主要是办法列表的循环(循环主要是选用二分法算法).

四、办法的慢速查找流程

上一小节中假如从办法列表中找到,履行done,调用log_and_fill_cache函数,这个函数中完成了将办法刺进到缓存中。

iOS底层原理之方法调用的底层探究
iOS底层原理之方法调用的底层探究
假如办法列表中没有找到,就会将当时的类赋为父类去父类中找,先到父类的缓存中找,找不到就会再次进入for (unsigned attempts = unreasonableClassCount() ;;)循环到父类的办法列表中查找。
iOS底层原理之方法调用的底层探究
假如在父类的办法列表也没找到就到父类的父类查找,直到父类为nil,假如还没找到,就调用forward_imp(办法转发)。 总结:办法(实例办法和类办法)的慢速查找
lookUpImpOrForward函数\

  1. 先在当时类的methodList中查找,假如找到会进行缓存;
  2. 在当时类的methodList中没找到,去父类的cache中查找;
  3. 在父类的cache中没找到就会到父类的methodList中查找;
  4. 直到父类为nil,假如还没找到,就会调用forward_imp,即音讯的转发

五、总结

  1. OC里边目标调用办法实践上底层完成是objc_msgSend、objc_msgSendSuper等函数。该函数接纳两个最少两个参数,reciver+sel,第一个参数表示音讯接纳者,经过该参数去找isa,找cache,找办法列表。第二个参数是办法名。
  2. 在办法的快速查找便是从cache中查找,而cache的底层完成是用的汇编, 汇编相对更快。
  3. 在办法的慢速查找中,会再次从cache中找到(主要处理多线程操作的时分,cache里边或许刺进了这个办法),然后再去类目标(经过reciver的isa指针找到类目标)办法列表中查找,这个查找办法主要是对办法列表的循环(选用的二分查找算法),找到则返回。假如没有找到,就去父类查找,将父类赋值给当时类,重复上述动作,直到父类不存在,最终找到imp则存入缓存中,假如父类没有找到则进入到forward_imp(即音讯转发)。

有任何问题,欢迎大家评指出哦!觉得写的不错的,费事点个赞哦