Runtime 介绍
runtime称为运行时,它区别于编译时
运行时是代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段编译时是源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段
runtime的使用有以下三种方式,其三种实现方法与编译层和底层的关系如图所示
- 通过
OC代码,例如[person sayNB] - 通过
NSObject方法,例如isKindOfClass - 通过
Runtime API,例如class_getInstanceSize
其中的compiler就是我们了解的编译器,即LLVM,例如OC的alloc 对应底层的objc_alloc, runtime system libarary 就是底层库
探索方法的本质
方法的本质
通过clang编译的源码,理解了OC对象的本质,同样的,使用clang编译main.cpp文件,通过查看main函数中方法调用的实现,如下所示
//main.m中方法的调用
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];
//👇clang编译后的底层实现
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
通过上述代码可以看出,方法的本质就是objc_msgSend消息发送
为了验证,通过objc_msgSend方法来完成[person sayNB]的调用,查看其打印是否是一致
注:
1、直接调用objc_msgSend,需要导入头文件#import <objc/message.h>
2、需要将target --> Build Setting -->搜索msg-- 将enable strict checking of obc_msgSend calls由YES改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
LGPerson *person = [LGPerson alloc];
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];
其打印结果如下,发现是一致的,所以 [person sayNB]等价于objc_msgSend(person,sel_registerName("sayNB"))
对象方法调用-实际执行是父类的实现
除了验证,我们还可以尝试让person的调用执行父类中实现,通过objc_msgSendSuper实现
- 定义两个类:LGPerson 和 LGTeacher,父类中实现了sayHello方法
- main中的调用
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[person sayHello];
struct objc_super lgsuper;
lgsuper.receiver = person; //消息的接收者还是person
lgsuper.super_class = [LGTeacher class]; //告诉父类是谁
//消息的接受者还是自己 - 父类 - 请你直接找我的父亲
objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));
objc_msgSendSuper方法中有两个参数(结构体,sel),其结构体类型是objc_super定义的结构体对象,且需要指定receiver 和 super_class两个属性,源码实现 & 定义如下
-
objc_msgSendSuper方法参数 -
objc_super源码定义
打印结果如下
子类方法调用转为执行父类的实现的打印结果
发现不论是[person sayHello]还是objc_msgSendSuper都执行的是父类中sayHello的实现,所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。
带着我们的猜测,下面我们来探索objc_msgSend的源码实现
objc_msgSend 快速查找流程分析
在objc4-781源码中,搜索objc_msgSend,由于我们日常开发的都是架构是arm64,所以需要在arm64.s后缀的文件中查找objc_msgSend源码实现,发现是汇编实现,其汇编整体执行的流程图如下
objc_msgSend 汇编源码
objc_msgSend是消息发送的源码的入口,其使用汇编实现的,_objc_msgSend源码实现如下
主要有以下几步
-
【第一步】判断
objc_msgSend方法的第一个参数receiver是否为空-
如果支持
tagged pointer,跳转至LNilOrTagged,- 如果
小对象为空,则直接返回空,即LReturnZero - 如果
小对象不为空,则处理小对象的isa,走到【第二步】
- 如果
-
如果即不是小对象,
receiver也不为空,有以下两步- 从
receiver中取出isa存入p13寄存器, - 通过
GetClassFromIsa_p16中,arm64架构下通过isa & ISA_MASK获取shiftcls位域的类信息,即class,GetClassFromIsa_p16的汇编实现如下,然后走到【第二步】
- 从
-
- 【第二步】获取isa完毕,进入慢速查找流程
CacheLookup NORMAL
主要分为以下几步
-
【第一步】通过
cache首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址占8字节,superClass占8字节),获取cahce,cache中高16位存mask,低48位存buckets,即p11 = cache -
【第二步】从cache中分别取出buckets和mask,并由mask根据哈希算法计算出哈希下标
-
通过
cache和掩码(即0x0000ffffffffffff)的&运算,将高16位mask抹零,得到buckets指针地址,即p10 = buckets -
将
cache右移48位,得到mask,即p11 = mask -
将
objc_msgSend的参数p1(即第二个参数_cmd)& msak,通过哈希算法,得到需要查找存储sel-imp的bucket下标index,即p12 = index = _cmd & mask,为什么通过这种方式呢?因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取,如下所示
-
-
【第三步】根据所得的
哈希下标index和buckets首地址,取出哈希下标对应的bucket- 其中
PTRSHIFT等于3,左移4位(即2^4 = 16字节)的目的是计算出一个bucket实际占用的大小,结构体bucket_t中sel占8字节,imp占8字节 - 根据计算的哈希下标
index 乘以单个bucket占用的内存大小,得到buckets首地址在实际内存中的偏移量 - 通过
首地址 + 实际偏移量,获取哈希下标index对应的bucket
- 其中
-
【第四步】根据获取的
bucket,取出其中的imp存入p17,即p17 = imp,取出sel存入p9,即p9 = sel -
【第五步】第一次递归循环
-
比较获取的
bucket中sel与objc_msgSend的第二个参数的_cmd(即p1)是否相等 -
如果
相等,则直接跳转至CacheHit,即缓存命中,返回imp -
如果不相等,有以下两种情况
- 如果一直都找不到,直接跳转至
CheckMiss,因为$0是normal,会跳转至__objc_msgSend_uncached,即进入慢速查找流程 - 如果
根据index获取的bucket等于buckets的第一个元素,则人为的将当前bucket设置为buckets的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到bucker的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环),即【第六步】 - 如果
当前bucket不等于buckets的第一个元素,则继续向前查找,进入第一次递归循环
- 如果一直都找不到,直接跳转至
-
-
【第六步】第二次递归循环:重复【第五步】的操作,与【第五步】中
唯一区别是,如果当前的bucket还是等于 buckets的第一个元素,则直接跳转至JumpMiss,此时的$0是normal,也是直接跳转至__objc_msgSend_uncached,即进入慢速查找流程
以下是整个快速查找过程值的变化过程