Runtime 介绍

runtime称为运行时,它区别于编译时

  • 运行时代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段
  • 编译时源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段

runtime使用有以下三种方式,其三种实现方法与编译层和底层的关系如图所示

  • 通过OC代码,例如 [person sayNB]
  • 通过NSObject方法,例如isKindOfClass
  • 通过Runtime API,例如class_getInstanceSize

Ios编译器预加载 ios编译原理_父类

 

其中的compiler就是我们了解的编译器,即LLVM,例如OC的alloc 对应底层的objc_allocruntime 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 callsYES 改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错

LGPerson *person = [LGPerson alloc];   
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];

其打印结果如下,发现是一致的,所以 [person sayNB]等价于objc_msgSend(person,sel_registerName("sayNB"))

Ios编译器预加载 ios编译原理_父类_02

 对象方法调用-实际执行是父类的实现

除了验证,我们还可以尝试让person的调用执行父类中实现,通过objc_msgSendSuper实现

  • 定义两个类:LGPerson 和 LGTeacher,父类中实现了sayHello方法
  • Ios编译器预加载 ios编译原理_递归_03

  • 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定义的结构体对象,且需要指定receiversuper_class两个属性,源码实现 & 定义如下

  • objc_msgSendSuper 方法参数
  • objc_super源码定义

打印结果如下

Ios编译器预加载 ios编译原理_Ios编译器预加载_04

子类方法调用转为执行父类的实现的打印结果

发现不论是[person sayHello]还是objc_msgSendSuper都执行的是父类sayHello的实现,所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。

带着我们的猜测,下面我们来探索objc_msgSend的源码实现

objc_msgSend 快速查找流程分析

在objc4-781源码中,搜索objc_msgSend,由于我们日常开发的都是架构是arm64,所以需要在arm64.s后缀的文件中查找objc_msgSend源码实现,发现是汇编实现,其汇编整体执行的流程图如下

Ios编译器预加载 ios编译原理_Ios编译器预加载_05

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位域的类信息,即classGetClassFromIsa_p16的汇编实现如下,然后走到【第二步】
  • 【第二步】获取isa完毕,进入慢速查找流程CacheLookup NORMAL

主要分为以下几步

  • 【第一步】通过cache首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址8字节,superClass8字节),获取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时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取,如下所示
  • 【第三步】根据所得的哈希下标indexbuckets首地址,取出哈希下标对应的bucket
  • 其中PTRSHIFT等于3,左移4位(即2^4 = 16字节)的目的是计算出一个bucket实际占用的大小,结构体bucket_tsel8字节,imp8字节
  • 根据计算的哈希下标index 乘以 单个bucket占用的内存大小,得到buckets首地址在实际内存中的偏移量
  • 通过首地址 + 实际偏移量,获取哈希下标index对应的bucket
  • 【第四步】根据获取的bucket,取出其中的imp存入p17,即p17 = imp,取出sel存入p9,即p9 = sel
  • 【第五步】第一次递归循环
  • 比较获取的bucketselobjc_msgSend的第二个参数的_cmd(即p1)是否相等
  • 如果相等,则直接跳转至CacheHit,即缓存命中,返回imp
  • 如果不相等,有以下两种情况
  • 如果一直都找不到,直接跳转至CheckMiss,因为$0normal,会跳转至__objc_msgSend_uncached,即进入慢速查找流程
  • 如果根据index获取的bucket 等于 buckets的第一个元素,则人为的将当前bucket设置为buckets的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到bucker的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环),即【第六步】
  • 如果当前bucket不等于buckets的第一个元素,则继续向前查找,进入第一次递归循环
  • 【第六步】第二次递归循环:重复【第五步】的操作,与【第五步】中唯一区别是,如果当前的bucket还是等于 buckets的第一个元素,则直接跳转至JumpMiss,此时的$0normal,也是直接跳转至__objc_msgSend_uncached,即进入慢速查找流程

以下是整个快速查找过程值的变化过程

Ios编译器预加载 ios编译原理_递归_06