iOS底层原理(二):RunTime底层原理

前言

OC是一种动态语言,其动态性是由Runtime API来支撑的,Runtime API提供的接口都是C语言的 ,源码由C、C++、汇编语言编写,想深入学习Runtime,需要先了解它底层的一些数据结构,例如isa指针

一、isa指针
    1. 每一个继承自NSObject的对象都有一个isa指针,通过isa指针我们可以拿到类/元类的内存地址
    1. arm64架构之前,isa就是一个普通的指针,直接指向类对象或者元类对象,isa直接存储着类对象、元类对象的内存地址
    1. arm64架构开始,对isa指针做了优化,变成了一个union共用体,使用了位域来存储更多的信息,isa指针内部结构如下所示,类对象/元类对象的地址存储在shiftcls位shiftcls位占了33位,由于是共用体,所以需要对isa进行一次&ISA_MASK的位运算,才能将类对象的地址取出来 (为何进行一次按位与&的运算就能取出来shiftcls位呢???,别着急,下面会有讲到)
      优化后的isa.png
    1. 所谓共用体,就是指多个成员共用同一段内存,跟结构体对比一下,就容易理解了:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
    1. 为何要进行一次&ISA_MASK的位运算才能将类对象的内存地址拿出来呢?看看下面的计算过程就明白了,按位与&的规则是:相同位的两个数字都为1,则为1;若有一个不为1,则为0
想把中间四位取出来,应该怎么取呢?
   1010  0101
&  0011  1100
----------------------
   0010  0100

只需要进行一次 &00111100 位运算,就可以将中间四位取出来了

这个方法用与取isa的shiftcls位的原理是一样的,只需要 isa & ISA_MASK就可以将shiftcls位的33位给取出来了
    1. isa指针占8个字节,一共有64位,每一位都有其特殊含义,如下图所示:
      image.png
二、Class的结构
    1. 我们知道isa指针是指向类或者元类的,而类和元类的底层数据结构就是objc_class结构体,objc_class的内部结构如下所示:
      objc_class结构体
    1. class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
      class_rw_t结构体
    1. class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容,如下图所示:
      class_ro_t结构体
    1. 上述的方法列表中,都用到了method_t结构体,method_t结构体是对方法的封装,其内存布局如下所示,其中IMP代表函数的具体实现;SEL代表方法名,一般叫做选择器,底层结构跟char *类似,不同类中相同名字的方法,所对应的方法选择器是相同的,可以通过@selector和sel_registerName()获得;types包含了函数返回值、参数编码的字符串,iOS提供了一个叫做@encode的指令,可以将具体类型表示成字符串编码
      method_t结构体.png

      OC类型编码.png
三、方法缓存
    1. Class内部结构中有个方法缓存cache_t,用散列表来缓存曾经调用过的方法,提高了方法的查找速度cache_t的内部结构如下所示,其中,_bucketsbucket_t结构体的数组,bucket_t是用来存放方法的SEL内存地址和IMP_mask的大小是数组大小 - 1;_occupied是当前已缓存的方法数,即数组中已使用了多少位置
      cache_t结构体.png
    1. 散列表,也叫哈希表,利用了数组支持下标随机访问的特性,通过散列函数把元素的键值key映射为数组的下标,然后把数据存储在下标对应的位置。按照键值key查找数据时,只需要用同样的散列函数,就可以把key转化为数组下标,进而从数组下标的位置取到数据,时间复杂度为O(1),如下图所示:
将key经过散列函数转化为数组下标.png
    1. 方法缓存cache_t就是用散列表来缓存曾经调用过的方法的,使用的散列函数是@selector(方法名) & _mask的位运算,其中@selector(方法名)是方法选择器,_mask散列表长度 - 1,将两者进行一次按位与的位运算,是为了快速算出来下标的同时,保证下标不越界
    1. 方法缓存到 散列表 的整个存储流程是这样的:
    • (1). 当某个方法被调用时,就会先看方法缓存cache_tbuckets中有没有此方法:

      • 缓存中有此方法的话,就直接取出来地址然后调用,不再走方法查找流程;

      • 缓存中没有的话,就会走方法查找流程,找到方法的IMP,调用此方法的同时,将方法地址缓存下来;

    • (2). 方法缓存的时候,会先看cache_t中的buckets有没有初始化:

      • 如果cache_t中的buckets已经初始化了,就会通过@selector(方法名) & _mask的位运算,计算出数组下标,然后将Key和IMP包装成bucket_t结构体,插入到buckets数组的对应的下标的位置;

      • 如果cache_t中的buckets没有初始化,就会给cache_t中的buckets分配大小为4的数组,并设置_mask为3,然后通过@selector(方法名) & _mask的位运算,计算出数组下标再插入

    • (3). 插入到buckets数组的对应的下标的位置的时候,会看此位置有没有被占用:

      • 如果下标对应的数组位置是空的,就直接将包装好的bucket_t结构体插入进去

      • 如果下标对应的数组位置有值了,就将数组下标 - 1,看看这个新位置是不是空的,如果是空的就插入进去;如果不是空的,就继续将数组下标 - 1,然后比较插入,直到数组下标 < 0,这个时候就将数组下标设置为_mask,继续整个插入过程 (_mask上面说了是数组的长度 - 1,所以不会有越界的风险)

    • (4). 如果buckets数组满了,就会进行扩容,扩容为原来大小的2倍 ,并且会将原来缓存的方法清空

    1. 在方法缓存的 散列表中 查找某个方法的流程是这样的:
    • (1). 调用某个对象的方法时,会向这个对象发送一个SEL消息,假设这个方法是:@selector(test)

    • (2). Runtime会去objc_class结构体cache方法缓存中找,会拿@selector(test)作为Key进行一次散列函数计算,散列函数是@selector(方法名) & _mask的位运算,经过散列函数计算出数组的下标,假设此时算出来的下标 == 2,如下图所示

    • (3).就会buckets数组的下标为2的位置取出来Key,与@selector(test)进行比较:

      • 如果Key相同,说明找对了,就会拿这个Key的IMP去调用;

      • 如果Key不相同,就将下标 - 1,继续寻找相同的Key,直到数组下标 < 0,这个时候就将数组下标设置为_mask,继续整个查找过程 (_mask上面说了是数组的长度 - 1,所以不会有越界的风险)

buckets数组.png
    1. 方法缓存的散列表,是通过开放寻址法来解决散列冲突的,所谓散列冲突,就是key不同的时候,散列值hash(key)却意外的相同了,方法缓存的散列冲突就是指,两个不同的Key,经过散列函数hash(key):@selector(方法名) & _mask算出来了同一个数组下标,这时候就出现了散列冲突,就将数组下标 - 1,依次往后查找。利用散列表缓存方法,虽然会浪费一些存储空间,但是却大大提升了方法查找速度,这也是空间换时间设计思想的具体应用
四、OC消息发送

OC中的方法调用,其实底层转换成了C语言objc_msgSend函数的调用,objc_msgSend的执行分为三大阶段:消息发送、动态方法解析、消息转发

    1. 消息发送
消息发送流程
    1. 动态方法解析
动态方法解析流程.png
动态方法解析时,增加方法.png
    1. 消息转发
消息转发流程.png
生成NSMethodSignature的两种方法
五、面试题
    1. 讲一下OC的消息机制

    答 :OC的方法调用其实都转成了objc_msgSend函数的调用,给receiver方法调用者发送了一条@selector(方法名)消息,objc_msgSend函数底层有三大阶段:消息发送、动态方法解析、消息转发

    1. OC的消息转发流程是怎么样的?
    • 先用调用forwardingTargetForSelecotor:获取另一个消息接受者,如果获取到了就给这个新的消息接受者,发送消息;

    • 如果获取不到新的消息接受者,就进入调用methodSignatureForSelector:获取方法签名,如果获取到了方法签名,就调用forwardInvocation:方法,在这个方法中可以自定义任何逻辑

    • 如果拿不到方法签名,就调用doesNotRecognizaSelector:方法,抛出异常

    1. RunTime有哪些具体应用?
    • 利用关联对象给分类增加属性

    • 遍历类的所有成员变量,实现字典转模型、自动归档解档

    • 交换方法实现

    • 利用消息转发机制,避免方法找不到而产生崩溃

你可能感兴趣的:(iOS底层原理(二):RunTime底层原理)