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有哪些具体应用?
    • 利用关联对象给分类增加属性

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

    • 交换方法实现

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342