iOS-底层原理 12:方法查询之快速查找

1,627 阅读5分钟

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 callsYES 改为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位域的类信息,即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时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取,如下所示

  • 【第三步】根据所得的哈希下标index 和 buckets首地址,取出哈希下标对应的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

  • 【第五步】第一次递归循环

    • 比较获取的bucketsel 与 objc_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,即进入慢速查找流程

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