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

2,460 阅读10分钟

本文主要内容

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这个函数。 image.png

扩展知识点
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的类型定义如下 image.png 手动实现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实现为主。代码部分截图如下 image.png 通过上面的代码我们知道objc_msgSend底层是用汇编实现的。

扩展知识点
1、为什么objc_msgSend底层使用汇编? 主要原因是汇编比C快,同时汇编会免去大量对局部变量的拷贝作用,参数被直接存放在寄存器中。
2、汇编会在函数和全局变量前加一个下划线_ 在程序中往往会包含汇编和C文件,对于编译器来说两者是一样的,因此可能会出现问题,为了防止符号名的冲突在汇编中函数和全局变量前会加一个下划线啊_(如上面代码截图里面) 接下来我们在我们上面创建的工程里面增加断点通过汇编的方式来配合源码进行查看objc_msgSend的方法快速查找流程。

image.png 在上图的位置增加断点,当断点执行到这个地方的时候,开始汇编调试(Debug -> Debug Workflow -> Always show Disassembly) image.png 按住Control,点击Step into单步调试 image.png 点击Step,进入objc_msgSend底层实现.
第1步 配合源码我们知道先判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend。通过读取寄存器证明此时的x0确实是LSTeacher对象. image.png 第2步 通过p13读取isa(下图1为objc源码objc_msgSend底层函数实现),图二里面x13就是LSTeacher实例对象isa的地址 image.png image.png 第3步 第二步取到对象isa地址之后,再通过isa取类对象,并保存到x16寄存器中,验证x16寄存器地址和LSTeacher类对象的地址相同,代码截图如下: image.png 第4步 从x16中取出类对象移到x15中,通过x16内存平移得到cache 图1是objc源码objc_msgSend函数实现。 image.png image.png 第5步 通过cache找存储方法的buckets。通过iOS底层原理之cache详解中通过源码分析cache的缓存内容的方法调试查找。 image.png 第6步 如果在cache中找到buckets,在buckets中找到对应的sel,就会调用cacheHit;如果没有找到,就会调用objc_msgSend_uncached函数。 image.png image.png 总结:方法(实例方法和类方法)的快速查找
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函数。 image.png image.png lookUpImpOrForward函数实现中,如下部分是系统为调用此函数做的准备工作。 image.png 接着,再一次从cache里面取找imp,这是为了防止多线程操作时刚好调用方法,此时缓存进来了。如果在cache还是找不到,就去方法列表中查找。 image.png 类的方法列表中查找具体查找代码如下: image.png image.png 使用二分法快速查找方法,结果可能找到也可能找不到,代码如下图 image.png 总结:当方法在cache里面找不到的时候会进入objc_msgSend_uncached函数,在这个函数里面会再次去cache里面找到(防止多线程问题),然后再去类对象方法列表中查找,查找主要是方法列表的循环(循环主要是采用二分法算法).

四、方法的慢速查找流程

上一小节中如果从方法列表中找到,执行done,调用log_and_fill_cache函数,这个函数中实现了将方法插入到缓存中。 image.png image.png 如果方法列表中没有找到,就会将当前的类赋为父类去父类中找,先到父类的缓存中找,找不到就会再次进入for (unsigned attempts = unreasonableClassCount() ;;) 循环到父类的方法列表中查找。 image.png 如果在父类的方法列表也没找到就到父类的父类查找,直到父类为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(即消息转发)。

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