怎么理解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_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,该函数不会执行有意义的操作,直接返回。
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 指针传送门
三次机会:动态方法解析, 消息接收者重定向, 消息重定向
一: 动态方法解析(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_唐姐让改名的博客-CSDN博客_runtime 时间上更新,class 结构体有改变,详细讲解ISA结构体类型。
深入浅出Runtime (一) 什么是Runtime? 定义? - 简书 里面有具体的class 类结构体的底层代码
iOS self和super底层实现原理 - 简书 self和super 的区别