Runtime & 消息转发机制

怎么理解OC是动态语言,Runtime又是什么?

静态语言:如C语言,编译阶段就要决定调用哪个函数,如果函数未实现就会编译报错。

动态语言:把一些决定性的工作从编译阶段推迟到运行时阶段。如OC语言,编译阶段并不能决定真正调用哪个函数,只要函数声明过即使没有实现也不会报错。

Runtime 可以OC代码编译转化为运行时代码,通过消息机制决定函数调用方式,是 OC 面向对象和动态机制的基石。

面向对象编程的三大特性是:封装、继承、多态

1.封装:抽象出数据类型和数据操作构成一个整体。需要类中有:成员变量、方法

2.继承:类之间的父子关系 ,OC中的类中存在一个isa指针和super_class指针,通过他们建立起了类之间的父子关系。

3. 多态:指针变量指向的具体类型,方法调用在运行时才能确定。

多态的三个必要条件是:继承、重写、父类指针可以指向子类对象。

OC是一门动态语言:

1. 可以在运行时新增方法(使用class_addMethod为类新增方法)

2. 可以改变类的结构(使用class_replaceMethod替换方法的实现等)

3. 运行时检查类型(运行时多态,id类型)

OC与Runtime的交互划分三种层次,交互程度从低到高排序:

1. OC源代码

2. NSObject方法

3. Runtime 函数

编写Runtime的时候会遇到没有提示的尴尬,那是因为在Xcode5.0以后的版本,Apple不建议我们写比较底层的代码,So,在target->buiglsetting搜索msg将YES改成NO,然后可以尽情的使用Runtime代码

Xcode-target-buiglsetting-搜索msg


类的runtime 底层实现:

class 结构体

class_rw_t: 提供了运行时对类拓展的能力。内容可以在运行时被动态修改的,可以说运行时对类的拓展大都是存储在这里的。(比如:分类) readwrite 类型 

 class_ro_t :存储的大多是类在编译时就已经确定的信息 readonly 类型: 成员变量ivar - list-t 在这个结构体中,在编译时已经确定,运行时不能再修改。

注意:二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。

class_rw_t 中使用的 method_array_t, property_array_t, protocol_array_t 都继承自 list_array_tt<Element, List>,   它可以不断扩张(类似于二位数组,数组里面可以存放数组,后续可把分类添加的方法,属性等加入进来),可以存储 list 指针。

存储的内容有三种:1. 空   2. 一个 entsize_list_tt 指针   3. entsize_list_tt 指针数组

支持category:在编译时--类的 method_list 会先插入链表,然后运行时 会把category的method_list再加入类的方法列表。

注意:category的method_list  运行时才被加入到类的方法列表中。方法调用时是顺序查找,category中重写原类方法会执行分类中的方法,原类方法还存在,但是不会被执行到。如果有多个分类,最晚编译的category中的方法会被执行。

分类详解: 分类了解 --传送门


Category 可以实现原始类的方法:

注意:这个不是覆盖,类中的方法和分类中的方法,都会在类的方法列表中存在。类中的新方法列表,是先放入分类的方法列表,然后再加入之前类的方法列表,所以分类中的方法会在前面,头插法。

category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的。找到后就执行了。不再继续查找。

所以如果分类实现了本类的方法,会调用分类的。如果有多个分类同时实现,那要看编译的时候哪个方法最后放入。

深入理解Objective-C:Category - 美团技术团队 ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ 里面有类新方法列表的组成方式。 

除  loadView  会先执行原类方法然后执行category外 其他都是执行category重写的方法。

category是无法添加成员变量的(因为在编译时期类对象的内存布局已经确定,如果在运行时 --添加成员变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。

方法定义是在objc_class中管理的,运行时 --不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用


realizeClass:realizeClass 处理后的类才是『真正的』类,调用时不能对类做写操作。

初始化之前,objc_class->data() 返回的指针指向 class_ro_t 结构体。等 static Class realizeClass(Class cls) 静态方法在类第一次初始化时被调用,它会开辟 class_rw_t 的空间,并将 class_ro_t 指针赋值给 class_rw_t->ro


其他字段大概含义:

1. ivars: 用于存放所有的成员变量和属性信息

 使用场景:我们在字典转换成模型的时候需要用到这个列表找到属性的名称,去取字典中的值,KVC赋值,或者直接Runtime赋值

2. methodLists: 用于存放对象的所有成员方法。

3. selector:方法选择器,编译时,会依据类名,方法名字、参数序列等,生成一个唯一的整型标识 (Int类型的地址),用来区分方法的 ID,selector 方法选择器名称不区分 +,-方法,这个ID是 SEL 类型,是个映射到方法的C字符串

注意

1. 不同类中相同名字的方法对应的方法选择器是相同的。

2.即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。因此 Objc 中方法命名有时会带上参数类型来进行区分。

3. 同一个类,方法名不能重复。不同的类,可以重复

获取SEL有 3 种方法:

1.  OC中,使用@selector(“方法名字符串”) 或者  使用NSSelectorFromString(“方法名字符串”)      

2.  Runtime方法,使用sel_registerName(“方法名字符串”)

 IMP:函数指针,指向方法的实现。

注意:避开消息发送,直接获取方法的地址并调用会更高效。 NSObject 类中有methodForSelector: 实例方法。可以用它来获取某个方法选择器对应的IMP地址。

方法结构体

当objc_msgSend找到方法的实现时,将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,  任何方法默认都有两个隐式参数:是在代码被编译时被插入实现中的。

1.  self:接收消息的对象。在当前方法中使用  self关键字, 引用实例本身, 

2:_cmd:方法选择器。  _cmd 和 imp 是一一对应的


Rutime消息发送基本原理

OC的方法调用都是类似 [receiver  selector] 的形式,其实是一个运行时消息发送过程。

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。

编译阶段:确定了要向哪个接收者发送message消息,编译器转化:

1.不带参数的方法被编译为:objc_msgSend(receiver,selector)

2.带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)

注意:编译器会根据情况在 objc_msgSend, objc_msgSend_stret,  objc_msgSendSuper, 或 objc_msgSendSuper_stret  四个方法中选择一个来调用。如果消息是传递给父类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。

在 i386 平台处理返回类型为浮点数的消息时,需要用到  objc_msgSend_fpret 函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend 不再适用。不过在 PPC 或 PPC64 平台是不需要麻烦它的。 

PS:带  “Super”  的是消息传递给父类;“stret”可分为“  st”+“ret”  两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。


运行时阶段:

1. 检测 selector 是不是需要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain, release 这些函数了。

2. 检测target 是不是nil 对象。ObjC 的特性是允许对一个 nil对象执行任何一个方法不会 Crash,因为会被忽略掉。如果传递给 objc_msgSend 的 self 参数是 nil,该函数不会执行有意义的操作,直接返回。

参考:Objc中向一个nil对象发送消息会怎样 - 简书

3. 满足上面两个条件后,通过 isa 指针找到它的 class ; 在类中查找 IMP,先从 cache 里面找,若找到就跳到对应的函数去执行。如果在cache里找不到就去类的 方法列表methodLists 里面找。

4. 如果找不到,就到顺着类的 superclass 指针去它父类的cache 和 methodLists方法列表里找,直到找到NSObject类为止,若找到就跳到对应的函数去执行。

5. 如果还找不到,Runtime就提供了三种方法来处理:动态方法解析消息接受者重定向消息重定向,如果还不能处理,就carsh 。 

注意: 在找到 method 方法后,把方法 的 method_name 作为key ,method_imp 作为value 给存起来。当再次收到这个消息的时候,可以直接在objc_cache 里找到,避免去遍历objc_method_list。

objc_cache 作用 :每次发消息都需要遍历一次 objc_method_list  并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。

参考⚠️⚠️⚠️:isa 指针传送门


三次机会:动态方法解析, 消息接收者重定向, 消息重定向

最后3次机会

一: 动态方法解析(Dynamic Method Resolution)

如果从本类到父类一层层的找也没有找到对应方法的话,就会走消息转发。

当通过 cache 和方法列表都没有找到方法时,Runtime 提供了 一次动态添加方法实现的机会,主要用到的方法如下:

动态添加方法
实现例子

在消息转发前会先走本类的方法 +resolveInstanceMethod:(处理找不到的实例方法)或+resolveClassMethod:(处理找不到的类方法)。在这个方法里面可以使用class_addMethod 函数向实例添加方法,使得消息发送能够正常进行。

注意:返回的BOOL值,无论返回什么,系统都会尝试再次用SEL找IML--(重复查找imp 方法地址的步骤,从cache 中查找开始 ),如果找到函数实现则执行函数。如果找不到继续其他查找流程。

参考:消息转发第一步resolveInstanceMethod返回YES or NO?_chenyingSunny的专栏-CSDN博客

2. 如果resolve方法返回 NO ,就会移到下一步,消息转发(Forwarding)的阶段forwardingTargetForSelector。

具体使用: IOS动态方法决议_snmhm1991的专栏-CSDN博客_动态方法决议

有一个标志位记录是否执行过 动态解析,如果执行过一次,下次进入动态解析直接进入消息转发下一流程,进入消息转发流程。

09.05-消息解析-resolveInstanceMethod_淡暗云之遥的博客-CSDN博客   


二:消息接收者重定向

Runtime 提供把这个消息转发给其他对象的机会。可以保证程序的继续执行。

重写列子

通过 forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。否则进入下一阶段,消息的重定向。

forwardingTargetForSelector 方法,返回一个指定的接收者

1. 返回nil,走转发流程第三步消息重定向

2. 返回非nil对象

   2.1:如果返回的对象可处理该方法,即使他自己没有该方法但是父类有,也可以,会执行该方法(其实就是消息发送流程)

    2.2:返回的对象无法处理该方法,接下来走返回对象的消息转发流程想,本次消息转发至此结束。 


三:消息重定向

Runtime 系统会通过 forwardInvocation 方法消息通知该对象,给予此次消息最后一次寻找IMP(可以被执行)的机会。

1. 每个对象都从 NSObject 类中继承了forwardInvocation 方法,

2. NSObject中forwardInvocation 方法只是简单的调用了doesNotRecongnizeSelector 方法,提示错误。

3. 重写 methodSignatureForSelector 和 forwardInvocation 方法:对不能处理的消息做一些默认处理避免崩溃,也可以将消息转发给其他对象来处理。

在forwardInvocation 消息发送前,Runtime系统会向对象发送methodSignatureForSelector消息,并取到返回的方法签名用于生成NSInvocation对象。

anInvocation :参数中的selector为导致crash的方法,target为导致crash的对象

消息转发实现

1. methodSignatureForSelector如果返回nil,会调用doesNotRecognizeSelector 方法--结束

2. methodSignatureForSelector 返回只要是非nil且是NSMethodSignature类型的任何值都可以,会调用forwardInvocation 方法 

4. forwardInvocation方法可以啥都不处理,或者做任何不会出问题的事,至此本次消息转发结束 

可以直接修改调用目标: 调用该方法的Target

//改变消息接受者对象 : [anInvocation invokeWithTarget: 想要改成的target ];

也可以修改调佣方法的SEL:重新替换一个新的SEL,调用另外一个方法

//改变消息的SEL    anInvocation.selector =   @selector(想要调用的方法); 

总结:

forwardInvocation方法: 像一个不能识别的消息的分发中心,可将消息同时转发给任意多个对象。也可将所有的消息都发送给同一个接收对象。或者对不同的消息提供同样的响应。

1. forwardingTargetForSelector 仅支持一个对象的返回,消息只能被转发给一个对象

2.  简单的”吃掉“某些消息,没有响应也没有错误。 理论上可以重载 doesNotRecognizeSelector 函数,重写使其不抛出异常(不调用super实现)。但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。

3. forwardInvocation 能够修改消息的内容,用于实现更加强大的功能。


使用场景:

向 nil 对象发送消息则不会产生崩溃

1. - ( NSMethodSignature * ) methodSignatureForSelector: ( SEL ) aSelector 

2.  - ( void ) forwardInvocation: ( NSInvocation * ) anInvocation 

重写 这两个方法将没能力处理消息的方法签名转发给 nil 对象则不会产生崩溃

处理崩溃拦截


转发消息3步的使用案例:继承自NSObject的不常用又很有用的函数(2) - 摇滚诗人 - 博客园

使用场景:runtime 使用场景

runtime相关api 讲解--使用案例  👍👍👍 👍👍👍

参考:

Objective-C Runtime | yulingtianxia's blog 详解class 类结构体中 rw  & ro 等字段的具体含义

Runtime-iOS运行时基础篇 - 简书 👍👍👍 

Runtime_唐姐让改名的博客-CSDN博客_runtime     时间上更新,class 结构体有改变,详细讲解ISA结构体类型。

Runtime整理

深入浅出Runtime (一) 什么是Runtime? 定义? - 简书  里面有具体的class 类结构体的底层代码

iOS self和super底层实现原理 - 简书  self和super 的区别 

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

推荐阅读更多精彩内容