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

719 阅读9分钟

本文主要内容

1.objc_msgSendSuper解析
2.方法的快速查找流程
3.方法的慢速查找算法
4.方法的慢速查找流程
5.关于方法查找的测试

前言

iOS底层原理之cache底层详解中研究了cache的底层,了解到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解析

创建一个macOS APP工程,添加HGPerson类及对应的方法(作者所建工程HGPerson中包含实例方法study:/happy,类方法eat),在main中新建HGPerson的实例对象并调用方法(此处先只调用实例方法)。然后在main.m文件目录下,打开终端输入clang -rewrite-objc main.m,编译main.m文件得到main.cpp文件,在此文件中找到main函数。

image.png

观察编译后的代码发现,调用方法实际上是调用了objc_msgSend函数,并且这个函数包含2个默认参数,第一个参数代表消息的接收者,第二个参数代表消息的方法名(sel)。objc_msgSend函数会根据消息的接收者和消息的方法名找到这个消息即方法的实现(imp)。如果实例方法本身有参数,编译后会跟在这2个参数后面传递。如果消息的接收者是类对象,就会根据类对象的isa指针指向元类对象找到元类,在元类中找对应的方法,如果消息的接收者是实例对象,就会根据实例对象的isa指针指向类对象找到类对象,在类对象中找对应的方法。 其实,我们也可以直接调用objc_msgSend函数来调用对应方法。

image.png

知识小亮点
查看main.cpp代码知道:
objc_msgSendSuper:发消息给对象的父类时会编译成此函数;
objc_msgSend_stret:发消息的返回值为结构体时会编译成此函数;
objc_msgSendSuper_stret:发消息给对象的父类的返回值为结构体时会编译成此函数;
objc_msgSend_fpret:发消息的返回值为浮点型时会编译成此函数;

接下来我们详细研究objc_msgSendSuper函数。创建新工程02_objc_msgSend Super,新建类HGPerson,其中添加实例方法study,新建继承自HGPerson的子类HGTeacher,重写init方法和study方法,在main中创建HGTeacher的实例对象并调用study方法,打印结果是什么呢?

image.png

image.png

实际打印结果如下:[self class]在编译时会转化为调用了objc_msgSend函数,所以输出HGTeacher[super class]在编译时转化为调用了objc_msgSendSuper函数,而该函数和objc_msgSend函数唯一的区别在于找方法的时候,前者是先从父类中找方法的实现。同理,所以[super study]打印出来的结果是先去父类中寻找该方法的实现,打印出-[HGPerson study]

image.png 为了更好的理解objc_msgSendSuper函数的调用机制,我们自己手动实现super关键字,创建类型为objc_super的结构体并为receiversuper_ class赋值,将objc_msgSendSuper函数转换objc_msgSendSuperTyped类型,然后找study方法的实现。

image.png

image.png

注意⚠️:上述自己实现super关键字的方法为参考网络方法。

当把super_class变为HGTeacher.class时,调用到objc_msgSendSuper函数时又会来HGTeacher类中找study方法的实现,所以就会循环调用HGTeacher类中的study方法。

image.png 当把super_class变为NSObject.class时,调用到objc_msgSendSuper函数时会去到NSObject类中找study方法的实现,但NSObject类并没有该方法,所以会crash

image.png

总结:objc_msgSendSuper函数和objc_msgSend函数唯一的区别在于找方法的时候,前者是先从父类中找方法的实现

二.方法的快速查找流程

结合objc4-838.1源码和汇编进行调试,跟踪study方法的查找流程。在[t study]处在添加断点,运行工程到此处后,在Debug -> Debug Workflow -> Always show Disassembly打开汇编调试工具,按住Control,点击Step into单步调试,进入objc msgSend函数底层实现。

image.png

image.png

验证objc_msgSend函数就是HGTeacher对象在调用study方法。

image.png

接下来正式进入查找流程。查看objc_msgSend函数源码(arm64)发现这个函数的底层是通过汇编实现的(598行),

知识小亮点
1.为什么objc_msgSend函数底层使用汇编?主要原因是汇编比c更块,同时汇编会免去大量对局部变量的拷贝操作 ,参数被直接存放在寄存器中。 
2.汇编会在函数和全局变量前加一个下划线"_",在程序中往往会包含汇编和C文件,对于编译器来说两者是一样的,因此可能会出现问题,为了防止符号名的冲突在汇编中函数和全局变量前会加一个下划线"_"。如图:

image.png

第一步,判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend。通过读取寄存器证明此时的x0确实是HGTeacher对象。

image.png

image.png

image.png

第2步,通过p13取到isa指针,通过打印实例对象t的内存地址验证确实是其isa指针地址。再通过isa取class并保存到p16寄存器中,读取p16寄存器得到的地址确实和HGTeacher类对象的地址相同。 image.png

image.png

第3步,从x16中取出class移到x15中,通过x16内存平移得到cache(x11)。 image.png

image.png

image.png

第4步,通过cache(x11)找存储方法的buckets。通过iOS底层原理之cache底层详解一、通过源码分析cache的缓存内容的方法调试查找。

image.png

第5步,如果在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函数(objc-msg-arm64->761行).

三.方法的慢速查找算法

上一节中,如果buckets中缓存没有sel方法,会调用objc_msgSend_uncached函数.现在来分析此函数。找到objc_msgSend_uncached函数源码,执行MethodTableLookup(objc-msg-arm64->735行),跳转到lookUpImpOrForward(objc_runtime_new->6446行)函数。

image.png image.png

lookUpImpOrForward函数实现中,如下部分是系统为调用此函数做的准备工作。

image.png 接着,再一次从cache里面取找imp,这是误了防止多线程操作时刚好调用方法,此时缓存进来了。如果在cache还是找不到,就去方法列表中查找。

image.png

最终到类的方法列表中查找,查找方法为二分法查找! image.png

image.png

通过遍历方法列表,使用二分法查找算法查找,结果可能为查找到或未查找到,如下图:

image.png

四.方法的慢速查找流程

三.方法的慢速查找算法中如果从方法类表中查找到,执行done,调用log_and_fill_cache函数,这个函数中实现了将该方法插入到缓存中。

image.png 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,即消息的转发

五.关于方法查找的测试

创建一个新工程03_方法查找测试,新建继承于NSObjectHGPerson类,没有任何方法,添加NSObject的分类NSObject+Test,在其中增加一个test方法。在main.m中使用HGPerson调用test方法,运行结果是怎样的呢?会崩溃吗?答案是不会崩溃,而是会正常调用test方法正常打印结果。这其实就是验证上述方法查找的一个过程:先在当前类查找,当前类找不到就去父类,直到父类存在方法及其实现!同时说明NSObject是万类之主

image.png

遗留问题

当直到父类为nil时仍然没有找到该方法,就会进行消息转发,这个消息转发是个什么过程呢?

本文总结

1.先在当前类查找,当前类找不到就去父类,直到(NSObject的父类)父类为nil,如果还没找到,就调用forward_imp(方法转发);
2.所有的方法查找过程都是为了查找到方法的实现,而不只是方法声明;
3.方法的查找过程中并没有对实例方法和类方法进行区分;
4.子类调用父类方法,其方法会缓存在子类中。

有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍