Runtime 的理解和应用

2019-04-18  本文已影响0人  你的代码掉了

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

静态语言: 如c语言,编译阶段就要决定调用那个函数,如果函数没有实现 就会编译报错.

动态语言:如oc语言,便于阶段并不能决定真正调用那个函数,只要声明过即可使用.没有实现也不会报错.

我们常说oc是一门动态语言,就是因为它总是把一些决定性的工作从编译阶段推迟到运行时阶段,oc代码的 运行不仅需要编译器,还需要运行时的系统(Runtime system)来执行编译后的代码.   

Runtime 是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时的代码,通过消息机制决定函数调用的方式,这也是   OC 作为动态语言的使用基础.

二,理解消息机制的基本原理.

OC的方法调用都是类似[receiver selector]的形式,其实每次都是消息发送的过程.

第一步:编译阶段

[receiver selector] 被编译器转化 分为两种情况: 有参数,无参数.

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

2.带参数的被编译为:objc_msgSender(receiver,selector,org1,org2,....)

第二步:运行时阶段

消息接收者recever 寻找对应的selector,也分为两种情况:

1.接收者能够找到对应的selector,直接执行selector方法.

2.接受者找不到对应的selector,消息被转发或者是临时向这个selector 添加实现内容,否则崩溃.

说明:   OC调用方法[receiver selector] 编译阶段确定了要向那个接受者发送message 消息.但是接收者如何响应是在运行时决定的.

三,与Runtime的交互

NSObject 方法

Runtime 最大的特性就是实现了OC语言的动态加载,作为大部分类的根类NSObject,其本身就具有了一些非常具有运行时动态特性的方法,例如:respondsToSelector:该方法能够检查代码在运行阶段当前的对象是否能相应指定的消息. 类似的方法还有:

- description://返回当前类的描述信息;

- class //返回对象的类

- iskingOfclass://属于某一个类

-methodForSelector //返回指定方法的实现的地址

使用Runtime 函数,分析Runtime 中的数据结构

oc代码被编译器转化为C语言,然后通过运行时执行,最总实现了动态调用,这其中oc的类,对象,和方法等都可以在Runtime 源码中找到他们的定义.

那么,我们来看下:

#import<objc/runtime.h>

#import<objc/message.h>

然后,我们需要使用组合键"Command +鼠标点击",即可进入Runtime的源码文件,下面我们继续来一一分析OC代码在C中对应的结构。

1.id ->objc_object

id 是一个指向objc_object结构体的指针,即 typedef struct objc_object *id;

下面是Runtime 中对obj_object的具体定义:

struct objc_object{

Class_Nonnull isa OBJC_ISA_AVALIABILITY;

};

我们都知道id在oc中是表示一个任意类型的实例,从这里也可以看出来,oc中的对象虽然没有明显的使用指针,但是在oc代码被编译器转化为c语之后,每个oc 对象其实都是拥有一个isa指针的.

Class ->objc_class

class 是一个指向objc_class结构体的指针,即在Runtime中:

struct objc_class *Class; 定义略过

isa指针:

我们会发现objc_class和objc_object同样是结构体,而且都拥有一个isa指针.我们很容易理解objc_object的isa指针指向对象的定义,那么objc_class的指针是怎么回事.?其实,在Runtime中objc类本身同时也是一个对象,Runtime把类对象所属类型叫做元类.用于描述类对象本身所具有的特征,最常见的类方法就被定义与此.所以objc_class中的isa指针指向的是元类. 每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类.

super_class 指针:

super_class指针指向objc_class指针指向objc_class类所继承的父类,但是如果当前类已经是最顶层的类,则super_class指针为NULL.

cache:

为了优化性能,objc_class中的cache结构体用于记录每次使用类或则对象调用的方法,这样每次响应消息的时候,runtime系统会优先在cache中寻找响应的方法,相比直接在类的方法列表中遍历查找,xiao率更高.

ivars:

ivars:

ivars用于存放所有成员变量对的属性信息.属性的存取方法都存放在methodlists中.

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

SEL

SEL 是一个指向objc_selector结构体的指针,SEL在OC中被称作方法选择器,用于表示运行时方法的名字,在编译的时候,会依据每一个方法的名字,参数序列,生成一个唯一的整形标识,这个标识就是SEL.

注意:

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

2.即使是同一个类中,方法名字相同而变量类型不同也会导致他们具有相同的方法选择器.

Ivar 代表类中实例变量的类型,是一个指向object_ivar结构体的指针,

Method 表示某个方法的类型,其里面的 的参数定义为:

method_name:方法名字

method_types:一个char指针,指向储存方法的参数类型和返回值类型.

method_imp:本质上是一个指针,指向方法的实现,

IMP 是一个函数指针,它指向了方法实现的首地址,当oc发起消息后,最终执行的代码是有IMP指针决定,利用这个特性,我们可以对代码进行优化,当需要大量重复调用方法的时候,我们可以绕开消息的绑定直接利用IMP  指针调起方法,这样的执行会更加高效.

深入理解Runtime 消息发送

OC调用方法被编译转化为如下的形式:

id_NULLable objc_msgSend(id_NULLable self,SEL _Nonnull op,...)

运行时阶段的消息发送的详细步骤如下:

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

2.检测target 是不是nil 对象,OBJC的特性是允许对一个 nil 对象执行任何一个方法不会crash,因为会被忽略.

3.如果上面两个都过了,那么就开始查找这个类的IMP,先从cache里面找,若可以找到就跳到对应的函数去执行.

4.如果class 里面找不到就找一下方法列表,method_Lists.

5.如果method_Lists找不到,就到超类的方法列表里寻找,一直找,直到找到NSObject类为止.

6.如果还是找不到,Runtime就会提供如下的三中方法来处理:动态方法解析,消息接受者重定向,消息重定向.这三种方法的调用关系如下:

消息转发流程图

1.动态方法解析(Dynamic Method Resolution)

所谓的动态方法解析,我们可以理解为通过cache和方法列表没找到方法时,Runtime 为我们提供的一次动态添加方法实现的机会,主要有一下方法:

+(BOOL)resolveClassMethod:(SEL)sel;//类方法没找到时,可用此方法实现

+(BOOL)resolveInstanceMerhod:(SEL)sel//实例方法没找到时调用

下面举个栗子:

类方法动态添加方法 实例方法动态添加实现方法

"v@"具体可以参考本链接.

消息接受者重定向:

我们注意到动态方法解析过程中的两个resolve方法都返回了布尔值,当他们返回YES(动态方法解析成功)时方法即刻正常运行,但是他们如果返回NO,消息发送机制就进入了消息转发(消息接受者重定向)的阶段了.我们可以使用Runtime 通过下面的犯方法替换消息接受者的为其他对象,杏儿保证的继续执行.

- (id)forwardingTargetForSelector:(SEL)aselector// 重定向类方法的消息接受者,返回一个类

-(id)forwardingTargetForSelector:(SEL)aselector//重定向一个实例方法的接受者,返回一个实例对象.

举个荔枝如下图:

类方法 重定向 实例方法重定向

消息重定向

如果动态解析,和消息接受者重定向返回的都是NO ,系统会通过Runtime 的消息转发机制,进行最后一次的IMP 寻找.被称为消息重定向

- (void)forwardInvcation:(NSInvocation *)anInvocation;

其实每个对象都从NSobject类中继承了forwardInvocation:方法 但是 NSObject中的只是简单地调用了doesNotRecongnizerSelector:方法  来提示 错误. 所以我们 可以重写这个方法,对不能处理的消息做一些默认处理,也可以将消息转发给其他对象来处理.而不是抛出错误.

我们注意到anInvocation 是 forwardInvocation:的唯一参数,它封装了原始的消息和消息参数.正事因为它,我们还不得不重写另一个函数.methodSignatureForSelector. 这是因为在forwardInvocation:消息发送前,Runtime 会向对象发送methodSignatureForSelector消息,并取到的方法签名用于生成NSInvocation对象.

举个荔枝:

消息重定向

1.从以上的代码中就可以看出,forwardingTargetForSelector仅支持一个对象的返回,也就是说消息只能被转发给一个对象,而forwardInvocation可以将消息同时转发给任意多个对象,这就是两者的最大区别。

2.虽然理论上可以重载doesNotRecognizeSelector函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。(If you override this method, you must call super or raise an invalidArgumentException exception at the end of your implementation. In other words, this method must not return normally; it must always result in an exception being thrown.)

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

多继承的实现思路:Runtime


多继承

参考文章:Runtime-iOS运行时基础篇

上一篇下一篇

猜你喜欢

热点阅读