前言
在上一章节cache底层探究中我们知道了cache是用来缓存方法的;当调用方法过后:
cache底层会会通过调用insert方法创建容器bucket_t并通过占用occupied来计算开辟容量capacity,这边我们需要注意occupied+2大于3/4容量的时候需要扩容到capacity*2;- 取到创建的
buckets,然后计算mask(等于capacity-1),通过mask和sel然后调用哈希函数cache_hash获取到存储方法时的开始位置; - 取到这个
begin后,会通过调用一个do-while循环来判断计算出的哈希表下标是否已经有值,防止哈希冲突,如果下表被占用,那么再次哈希cache_next,最后讲方法存储到哈希表中。
补充
bucket_t类型
上一节中我们探究cache_t里面的bucket_t数据时是以下这种方式:
通过buckets()[1]来进行取值,这个不就是数组下标取值吗?那意味着buckets()就是一个数组?很显然我们拿出来说,那肯定就不是一个数组下标取值那么简单了,哈哈,一起来看一下bucket_t底层结构是什么,
很明显是一个结构体,那既然是结构体,我们通过buckets()[index](index为数字下标)取值是什么意思呢?
- 首先
buckets()就等于bucket_t *就是个结构体指针; [index]其实指的是取内存平移;buckets()是个结构体指针,也就是在这个指针的基础上平移一个单位,其实就相当于buckets()+index;
###_bucketsAndMaybeMask里面value值的意义
我们通过p/x一下这个value值
所以我们可以得出,其实
_bucketsAndMaybeMask里面value值就是对应的buckets的首地址,知道这个buckets首地址那么就可以获取到其他的bucket了,这样做的好处就是使得cache_t里面更加结构清晰,不占用很多的内存,用户一旦想要知道其他bucket,只需要根据首地址,然后做偏移对应单位即可。
###3/4扩容的意义
- 有利于空间利用率
- 有效避免哈希冲突
Cache缓存方法闭环流程
知道了cache其里面有一个insert方法用来缓存方法,那么到底是谁唤起这个insert方法的呢?cache写入和读取到底是一个什么流程呢?
在objc源码中搜索insert,我们发现有很多的insert方法,到底是哪一个方法里面调用了cache的insert方法呢,我们找的也很难受,这个时候断点调试查看调用堆栈就是最好的方法了:
cache的insert方法堆栈分析
我们在cache_t::insert方法里打下一个断点:
然后运行,通过
LLDB指令bt查看调用堆栈,当然也可以直接看左边的调用流程:
查看堆栈发现在cache::insert前是调用了一个log_and_fill_cache,搜索一下看看
然后顺着log_and_fill_cache我们又可以发现是在lookUpImpOrForward方法中调用的
可以发现这边就开始牵扯到我们的消息发送了,而查看objc_cache.mm文件上方注释也能看出来,所以探究之前我们有必要先了解一下objc_msgSend.
Runtime探究
在了解objc_msgSend前我们先需要知道iOS中的运行时runtime;
编译时
在日常开发中我们基本都是用更高级的语言在写项目,像iOS中我们就是使用OC或者Swift语言,但是对于机器来说,它们根本识别不了高级语言,所以这个时候就有了编译器,编译器通过编译高级语言,将其转化为机器能够识别的底层语言的过程我们就可以称之为编译时
- 编译器工作:词法分析,语法分析等,也就是编译器类型检查(也称静态类型检查,所以这时代码还没有被装载到内存运行起来,只是将代码当做文本扫描一遍)。
运行时(Runtime)
runtime定义
有了编译时,那对应就有了运行时,运行时也就是代码已经装载到内存中运行起来了,这时候也会有类型检查,一般称为运行时类型检查,也就是在内存在动态操作类型;就像在iOS中正是因为有了运行时runtime,由于在运行时才加载到内存中,所以我们在这期间可以实现很多我们想要的东西,动态添加属性,添加方法等等。
modern和legacy两个版本,我们现在用的是对应objective-c 2.0编程接口的modern现行版本;主要适用于iPhone程序和Mac OS X v10.5及以后的系统中的64位程序。
runtime调起方式
Objective-C Code也就是我们OC代码层,即源码;FrameWork&Serivce框架服务层;Runtime Api也就是我们的一些objc_xxx,class_xxx等;Compiler编译层;Runtime System Libraryruntime的一些底层库。
- 直接通过
OC代码调用:例如调用对象方法[person sayNB]; - 通过
NSObject接口:例如调用isKindOfClass方法; - 通过
Runtime Api:例如class_getInstanceSize等
下面我们通过代码来验证一下看看:
声明两个类,LhkhPerson继承自NSObject,LhkhTeacher继承自LhkhPerson
OC方法调用
然后我们通过clang -rewrite-objc main.m -o main.cpp 生成main.cpp文件,在文件中我们可以查找到main方法里面方法调用:
我们可以看到在编译过后对上层的OC代码相当于做了一个解释,并且可以看出都是通过objc_msgSend消息发送。
补充
我们知道只在类中声明方法,而没有实现的话,对象调用该方法必会报错,但是我们这边父类中声明和实现了该方法后子类对象调用并没有报错,而且还能调用到父类该方法中这是为什么呢?
我们在main.cpp文件中搜索objc_msgSend时我们会发现:
那这个objc_msgSendSuper作用是啥呢?我们直接调用一下这个方法看看呢,但是不知道需要些什么参数啊,我们通过objc源码看一下
里面有两个参数 objc_super * 和 SEL op;op好理解,是SEL类型,也就是我们的方法名,那么objc_super *这是个什么呢?
所以这个结构体中也就两个参数,receiver和super_class,receiver也就是我们的消息接受者,super_class即消息接受者的父类(这边注意下面对super_class的注释,/* super_class is the first class to search */表示调用的方法会第一个查找你传入的类,如果没有会根据继承链继续找),然后试着调用一下发现果然是成功的。
OC代码调用方法时调起Runtime总结:
- 调用方法=消息发送:
objc_msgSend(消息接受者,消息主体(sel+参数)); - 如果调用的是类方法那么在
objc_msgSend(消息接受者,消息主体(sel+参数))方法中消息接受者底层就会转化为(id)objc_getClass("xxx")(xxx为类名),如果是对象方法则直接就是(id)xxx(xxx为对象名); - 调用的方法名在底层会通过
sel_registerName方法进行注册,如果调用的方法中有参数,那么消息主体中就会有参数; - 调用的方法会根据继承链去查找方法实现。
NSObject 接口
直接使用
NSObject提供的接口方法performSelector也是可以直接调起objc_msgSend消息发送的。
Runtime Api
通过
Runtime Api直接调用。
注意使用Runtime Api的时候,首先需要导入头文件#import <objc/message.h>,其次编译会报错,这个时候需要设置一下:
objc_msgSend探究
通过断点我们查看一下objc_msgSend源码在哪个里面:
在调用方法处我们添加一个断点,然后通过debug workflow勾选住显示汇编,通过按住control,点击控制台step into 进入到汇编描叙
可以看出源码也是在libobjc中(objc源码链接和怎么配置可以查看前面章节);
通过直接搜索objc_msgSend,我们会发现太多了,通过前面我们了解到objc_msgSend是通过汇编写的,那么我们直接找汇编文件(.s文件)
首先注意一下这些宏定义
接下来就是开始分析一下这些汇编代码了:
- 第一步进入到
objc_msgSend方法中; - 比较当前
receiver地址和0作比较,然后判断是否支持TAGGED_POINTERS类型,查看SUPPORT_TAGGED_POINTERS宏定义我们知道在64位系统中是支持的:- 如果小于等于0,那么就会走LNilOrTagged;
- 如果等于0,那么就会走LReturnZero,直接返回; 给一个空对象发送消息,是没有意义的;
- 如果上面均不满足,则将
x0寄存器里面的地址读取出来给p13,其实这边的x0存储的就是receiver的首地址,即isa的地址(isa中有类的信息,我们这边是探究cache,而cache在类中,所以拿到isa去获取类信息); - 通过宏定义
GetClassFromIsa_p16,并传递p13,1,x0三个参数,从而获取到class,也就是将class存放到p16中; - 最后通过获取到
isa标识LGetIsaDone,调用CacheLookup,并传递三个参数NORMAL,_objc_msgSend,__objc_msgSend_uncached。
这其中就有几个点,GetClassFromIsa_p16是怎么操作使得class存储到p16的呢,我们看一看
GetClassFromIsa_p16
ExtractISA
- 首先判断
SUPPORT_INDEXED_ISA(通过搜索能查找到这个宏主要是判断armv7korarm64_32),显然我们这边是arm64_64位,所以直接走下面__LP64__; needs_auth传递过来是1,所以就直接走的ExtractISA p16, \src, \auth_address;ExtractISA也是一个宏定义,and $0, $1, #ISA_MASK根据在上面GetClassFromIsa_p16的调用传递过来的参数,$0 = p16,\src = $1(\src就是isa),解释就是$0 = $1 & ISA_MAS,而我们前面已经知道了isa与上掩码得到的就是class,所以这边就是将class存到$0,也就是p16.
总结
通过上面对Runtime消息发送objc_msgSend底层汇编代码的分析,我们知道通过对象首地址获取到isa,然后取到class,最后再调用CacheLookup,那么到底是如何查找方法呢,一起期待下节分解。