美文网首页
Runtime 机制

Runtime 机制

作者: 马小峰MXF | 来源:发表于2018-10-29 10:06 被阅读0次

    内容参考:

    南峰子技术博客

    iOS底层原理总结 - Category的本质

    iOS底层原理总结 - 探寻Runtime本质(四)

    1、OC 消息发送机制/消息转发流程

    1.1 消息发送机制

    在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver message] 转化为一个消息函数的调用,即 objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示:

    这个函数完成了动态绑定的所有事情:

    1、首先它找到 selector 对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。

    2、它调用方法实现,并将接收者对象及方法的所有参数传给它。

    3、最后,它将实现返回的值作为它自己的返回值。

    消息的关键在于我们前面章节讨论过的结构体 objc_class,这个结构体有两个字段是我们在分发消息的关注的:

    1、指向父类的指针

    2、一个类的方法分发表,即 methodLists。

    当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。

    下图演示了这样一个消息的基本框架:

    当消息发送给一个对象时,objc_msgSend 通过对象的 isa 指针获取到类的结构体,然后在方法分发表里面查找方法的 selector。如果没有找到 selector,则通过 objc_msgSend 结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的 selector。依此,会一直沿着类的继承体系到达 NSObject 类。一旦定位到 selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到 selector,则会走消息转发流程,这个我们在后面讨论。

    为了加速消息的处理,运行时系统缓存使用过的 selector 及对应的方法的地址。

    1.2 消息转发流程

    当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。

    消息转发机制基本上分为三个步骤:

    1、动态方法解析

    2、备用接收者

    3、完整转发

    1.2.1 动态方法解析

            对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveInstanceMethod: (实例方法)或者 +resolveClassMethod: (类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法”。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod 函数动态添加到类里面就可以了。如下代码所示:

    不过这种方案更多的是为了实现 @dynamic 属性。

    1.2.2 备用接收者

    如果在上一步无法处理消息,则 Runtime 会继续调以下方法:

    如果一个对象实现了这个方法,并返回一个非 nil 的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是 self 自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理 aSelector,则应该调用父类的实现来返回结果。

    使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:

    这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

    1.2.3 完整消息转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

    运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的 NSInvocation 对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation 中,包括 selector,目标 (target) 和参数。我们可以在 forwardInvocation 方法中选择将消息转发给其它对象。

    forwardInvocation: 方法的实现有两个任务:

    1、定位可以响应封装在 anInvocation 中的消息的对象。这个对象不需要能处理所有未知消息。

    2、使用 anInvocation 作为参数,将消息发送到选中的对象。anInvocation 将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

    不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

    还有一个很重要的问题,我们必须重写以下方法:

    消息转发机制使用从这个方法中获取的信息来创建 NSInvocation 对象。因此我们必须重写这个方法,为给定的 selector 提供一个合适的方法签名。

    完整的示例如下所示:

    NSObject 的 forwardInvocation: 方法实现只是简单调用了 doesNotRecognizeSelector: 方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

    从某种意义上来讲,forwardInvocation: 就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

    1.2.4 消息转发与多重继承

    回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。

    不过消息转发虽然类似于继承,但 NSObject 的一些方法还是能区分两者。如respondsToSelector: 和 isKindOfClass: 只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

    2、category 扩展方法的底层本质

    Objective-C 中的 category 允许我们通过给一个类添加方法来扩充它(但是通过 category 不能添加新的实例变量)。

    Category 是表示一个指向分类的结构体的指针,其定义如下:

    这个结构体主要包含了分类定义的实例方法与类方法,其中 instance_methods 列表是objc_class 中方法列表的一个子集,而 class_methods 列表是元类方法列表的一个子集。

    Runtime 并没有在 <objc/runtime.h> 头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在 objc_class 中,我们可以通过针对 objc_class 的操作函数来获取分类的信息。如下例所示:

    其输出是:

    1、一个类可以有多个分类。分类信息存储在 category_t 结构体中,那么多个分类则保存在category_list 中。

    2、分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。

    问: Category 的实现原理,以及 Category 为什么只能加方法不能加属性?

    答:分类的实现原理是将 category 中的方法,属性,协议数据放在 category_t 结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。

    Category 可以添加属性,但是并不会自动生成成员变量及 set/get 方法。因为 category_t 结构体中并不存在成员变量。成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法在程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。

    附:Class

    Objective-C 类是由 Class 类型来表示的,它实际上是一个指向 objc_class 结构体的指针。它的定义如下:

    查看 objc/runtime.h 中 objc_class 结构体的定义如下:

    在这个定义中,下面几个字段是我们感兴趣的

    1、isa:需要注意的是在 Objective-C 中,所有的类自身也是一个对象,这个对象的 Class 里面也有一个 isa 指针,它指向 metaClass(元类,存储类方法列表)。

    2、super_class:指向该类的父类,如果该类已经是最顶层的根类(如 NSObject 或 NSProxy ),则 super_class 为NULL。

    3、cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是 methodLists 中遍历一遍,性能势必很差。这时,cache 就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到 cache 列表中,下次调用的时候 runtime 就会优先去 cache 中查找,如果 cache没有,才去 methodLists 中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

    4、version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

    3、super 关键字的底层本质

    面试题:下列代码中 Person 继承自 NSObject,Student 继承自 Person,写出下列代码输出内容。

    可以发现无论是 self 还是super调用class或superclass的结果都是相同的。

    下面探寻原理:在Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super,如下所示:

    如何使用 super 我们都知道。现在的问题是,它是如何工作的呢?

    首先我们需要知道的是 super 与 self 不同。self 是类的一个隐藏参数,每个方法的实现的第一个参数即为 self。而 super 并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用 viewDidLoad 方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self 指向的是相同的消息接收者。为了理解这一点,我们先来看看 super 的定义:

    这个结构体有两个成员:

    1、receiver:即消息的实际接收者

    2、superClass:指针当前类的父类

    当我们使用 super 来接收消息时,编译器会生成一个 objc_super 结构体。就上面的例子而言,这个结构体的 receiver 就是 MyViewController 对象,与 self 相同;superClass 指向MyViewController 的父类 UIViewController。

    接下来,发送消息时,不是调用 objc_msgSend 函数,而是调用 objc_msgSendSuper 函数,其声明如下:

    该函数第一个参数即为前面生成的 objc_super 结构体,第二个参数是方法的 selector。该函数实际的操作是:从 objc_super 结构体指向的 superClass 的方法列表开始查找 viewDidLoad 的selector,找到后以 objc->receiver 去调用这个 selector,而此时的操作流程就是如下方式了:

    由于objc_super->receiver就是self本身,所以该方法实际与下面这个调用是相同的:

    从上图中我们知道 super 调用方法的消息接受者 receiver 仍然是 self,只是从父类的类对象开始去查找方法。

    为了便于理解,我们看以下实例:

    调用 MyClass 的 test 方法后,其输出是:

    从上例中可以看到,两者的输出都是MyClass。

    我们知道 class 的底层实现如下面代码所示:

    class 内部实现是根据消息接受者返回其对应的类对象,最终会找到基类的方法列表中,而 self和 super 的区别仅仅是 self 从本类类对象开始查找方法,super 从父类类对象开始查找方法,因此最终得到的结果都是相同的。如果 super 不是从父类开始查找方法,而从本类查找方法的话,就调用方法本身造成循环调用方法而 crash

    同理 superclass 底层实现同 class 类似,其底层实现代码如下入所示:

    相关文章

      网友评论

          本文标题:Runtime 机制

          本文链接:https://www.haomeiwen.com/subject/ikovtqtx.html