本文主要内容
1.objc_msgSendSuper解析
2.方法的快速查找流程
3.方法的慢速查找算法
4.方法的慢速查找流程
5.关于方法查找的测试
前言
iOS底层原理之cache底层详解中研究了cache
的底层,了解到cache是用来缓存方法的
,缓存方法的目的
是为了使方法再次被调用时能快速响应。往cache
中缓存方法需要调用insert
函数,在objc4-838.1
源码的cache
源码中(62行)发现,编译器从cache
中调用方法要用到objc_msgsend
、cache_getImp
,如下图。消息发送就是runtime
通过sel
找到imp
的过程,消息发送在编译阶段编译器会把这个过程转换成objc_msgSend
函数。因此,接下来我们来详细了解objc_msgSend
这个函数。
知识小亮点
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
函数。
观察编译后的代码发现,调用方法实际上是调用了objc_msgSend
函数,并且这个函数包含2个默认参数,第一个参数代表消息的接收者,第二个参数代表消息的方法名(sel)。objc_msgSend
函数会根据消息的接收者和消息的方法名找到这个消息即方法的实现(imp)。如果实例方法本身有参数,编译后会跟在这2个参数后面传递。如果消息的接收者是类对象,就会根据类对象的isa指针指向元类对象找到元类,在元类中找对应的方法,如果消息的接收者是实例对象,就会根据实例对象的isa指针指向类对象找到类对象,在类对象中找对应的方法。
其实,我们也可以直接调用objc_msgSend
函数来调用对应方法。
知识小亮点
查看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
方法,打印结果是什么呢?
实际打印结果如下:[self class]
在编译时会转化为调用了objc_msgSend
函数,所以输出HGTeacher
,[super class]
在编译时转化为调用了objc_msgSendSuper
函数,而该函数和objc_msgSend
函数唯一的区别在于找方法的时候,前者是先从父类中找方法的实现
。同理,所以[super study]
打印出来的结果是先去父类中寻找该方法的实现,打印出-[HGPerson study]
。
为了更好的理解
objc_msgSendSuper
函数的调用机制,我们自己手动实现super
关键字,创建类型为objc_super
的结构体并为receiver
、super_ class
赋值,将objc_msgSendSuper
函数转换objc_msgSendSuperTyped
类型,然后找study
方法的实现。
注意⚠️:上述自己实现super关键字的方法为参考网络方法。
当把super_class
变为HGTeacher.class
时,调用到objc_msgSendSuper
函数时又会来HGTeacher
类中找study
方法的实现,所以就会循环调用HGTeacher
类中的study
方法。
当把
super_class
变为NSObject.class
时,调用到objc_msgSendSuper
函数时会去到NSObject
类中找study
方法的实现,但NSObject
类并没有该方法,所以会crash
。
总结:objc_msgSendSuper
函数和objc_msgSend
函数唯一的区别在于找方法的时候,前者是先从父类中找方法的实现
。
二.方法的快速查找流程
结合objc4-838.1
源码和汇编进行调试,跟踪study方法的查找流程。在[t study]处在添加断点,运行工程到此处后,在Debug -> Debug Workflow -> Always show Disassembly打开汇编调试工具,按住Control,点击Step into单步调试,进入objc msgSend
函数底层实现。
验证objc_msgSend
函数就是HGTeacher对象在调用study方法。
接下来正式进入查找流程。查看objc_msgSend
函数源码(arm64)发现这个函数的底层是通过汇编实现的(598行
),
知识小亮点
1.为什么objc_msgSend函数底层使用汇编?主要原因是汇编比c更块,同时汇编会免去大量对局部变量的拷贝操作 ,参数被直接存放在寄存器中。
2.汇编会在函数和全局变量前加一个下划线"_",在程序中往往会包含汇编和C文件,对于编译器来说两者是一样的,因此可能会出现问题,为了防止符号名的冲突在汇编中函数和全局变量前会加一个下划线"_"。如图:
第一步
,判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend。通过读取寄存器证明此时的x0确实是HGTeacher
对象。
第2步
,通过p13取到isa指针,通过打印实例对象t的内存地址验证确实是其isa指针地址。再通过isa取class并保存到p16寄存器中,读取p16寄存器得到的地址确实和HGTeacher类对象的地址相同。
第3步
,从x16中取出class移到x15中,通过x16内存平移得到cache
(x11)。
第4步
,通过cache
(x11)找存储方法的buckets
。通过iOS底层原理之cache底层详解中一、通过源码分析cache的缓存内容
的方法调试查找。
第5步
,如果在cache中找到buckets,在buckets中找到对应的sel,就会调用cacheHit;如果没有找到,就会调用objc_msgSend_uncached函数。
总结:方法(实例方法和类方法)的快速查找
objc_msgSend(receiver,sel,<其他参数>(方法的参数))
(1)判断receiver
是否存在;
(2)通过receiver
的isa
指针找到对应的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行
)函数。
lookUpImpOrForward
函数实现中,如下部分是系统为调用此函数做的准备工作。
接着,再一次从
cache
里面取找imp,这是误了防止多线程操作时刚好调用方法,此时缓存进来了。如果在cache
还是找不到,就去方法列表中查找。
最终到类的方法列表中查找,查找方法为二分法查找!
通过遍历方法列表,使用二分法查找算法查找,结果可能为查找到或未查找到,如下图:
四.方法的慢速查找流程
三.方法的慢速查找算法
中如果从方法类表中查找到,执行done
,调用log_and_fill_cache
函数,这个函数中实现了将该方法插入到缓存中。
如果在方法列表中也没找到,就会将当前类赋为父类去父类中找,先到父类的缓存中找,找不到就会再次进入for (unsigned attempts = unreasonableClassCount() ;;)
循环到父类的方法列表中查找。
如果在父类的方法列表也没找到就到父类的父类查找,直到父类为nil,如果还没找到,就调用forward_imp
(方法转发
)。
总结:方法(实例方法和类方法)的慢速查找
lookUpImpOrForward
函数
(1)先在当前类的methodList
中查找,如果找到会进行缓存;
(2)在当前类的methodList
中没找到,去父类的cache
中查找;
(3)在父类的cache
中没找到就会到父类的methodList
中查找;
(4)直到父类为nil,如果还没找到,就会调用forward_imp
,即消息的转发
。
五.关于方法查找的测试
创建一个新工程03_方法查找测试
,新建继承于NSObject
的HGPerson
类,没有任何方法,添加NSObject
的分类NSObject+Test
,在其中增加一个test
方法。在main.m
中使用HGPerson
调用test
方法,运行结果是怎样的呢?会崩溃吗?答案是不会崩溃,而是会正常调用test
方法正常打印结果。这其实就是验证上述方法查找的一个过程:先在当前类查找,当前类找不到就去父类,直到父类存在方法及其实现
!同时说明NSObject是万类之主
!
遗留问题
当直到父类为nil时仍然没有找到该方法,就会进行消息转发,这个消息转发是个什么过程呢?
本文总结
1.先在当前类查找,当前类找不到就去父类,直到(NSObject的父类
)父类为nil,如果还没找到,就调用forward_imp
(方法转发
);
2.所有的方法查找过程都是为了查找到方法的实现,而不只是方法声明;
3.方法的查找过程中并没有对实例方法和类方法进行区分;
4.子类调用父类方法,其方法会缓存在子类中。
有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍