Runtime - 消息发送原理.

Objective-C运行时的核心就在于消息分派器objc_msgSend,消息分派器把选择器映射为函数指针,并调用被引用的函数。 要想理解objc_msgSend的背后原理,先来理解下NSInvocation这个类。

NSInvocation是命令模式的一种传统实现,它把一个目标、一个选择器、一个方法签名和所有的参数都塞进一个对象里,这个对象可以先存储起来,以备将来调用。当NSInvocation被调用时,它会发送信息,Objective-C运行时会找到正确的方法实现来执行。我们通过一个例子来理解下NSInvocation的作用,比如[NSObject alloc],此时会发送一个alloc消息,这条消息都包含什么内容呢?它怎么找到alloc的实现方法呢?这些都是通过NSInvocation来完成的,它包含了消息要传递的内容,也告诉了该怎么找到对应的方法实现

解释一下什么是方法实现?一个方法实现(IMP)是一个指向具有如下签名的C函数的函数指针,注意是指针

1
id function(id self, SEL _cmd, ...)

NSInvocation包含了一个目标和选择器,目标是一个可接受的对象,选择器则是被发送的消息。比如[NSObject alloc],目标就是NSObject,选择器就是alloc。一个选择器大致是一个方法的名称,之所以说是大致是因为选择器不必精确映射到方法。比如[NSString length]和[NSData length]会映射到不同方法的实现,但他们拥有相同的选择器。

NSInvocation还包含一个方法签名(NSMethodSignature),它封装了一个方法的返回类型和参数类型,记住它不包括方法名称,只有返回类型和参数类型。你可以手动创建一个方法签名,如下:

1
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"@@:*"];

但是应该尽可能少使用signatureWithObjCTypes:方法,获得方法签名常用的方法是为它请求一个类或实例,比如可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名。两个方法有点绕口,我们通过一个例子来看下区别:

1
2
3
4
5
6
7
8
9
SEL initSEL = @selector(init);
SEL allocSEL = @selector(alloc);
 
// 从NSString类中获取实例方法(init)的方法签名
NSMethodSignature *initSig = [NSString instanceMethodSignatureForSelector:initSEL];
// 从test实例中获取实例方法(init)的方法签名
initSig = [@"test" methodSignatureForSelector:initSEL];
// 从NSString类中获取类方法签名
NSMethodSignature *allocSig = [NSString methodSignatureForSelector:allocSEL];

最后,NSInvocation还包含了所有的参数。至此,对于[NSString length]和[NSData length]就可以通过NSInvocation对象包含的信息,找到它们分别对应的方法实现。我们来看一个具体的例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
NSMutableSet *set = [NSMutableSet set];
NSString *stuff = @"stuff";
SEL selector = @selector(addObject:);
NSMethodSignature *sig = [set methodSignatureForSelector:selector];
44
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:set];
[invocation setSelector:selector];
[invocation setArgument:&stuff atIndex:2];
[invocation invoke];
44
NSLog(@"set is : %@", set);

注意,第一个参数被置于索引2处,索引0是目标(self),索引1是选择器(_cmd),NSInvocation会自动设置它们。另外,必须把参数指针传递给参数,而不能传递参数本身。

接下来重点要介绍下消息传递是如何工作的?

在Objective-C中调用方法最终会翻译成调用方法实现的函数指针,并传递给这个方法实现一个对象指针、一个选择器和一组函数参数。每个Objective-C消息表达式都会转化为对objc_msgSend的调用,看下objc_msgSend的工作方式:

1. 检查接受对象是否为nil,如果是nil,调用nil处理程序。

2. 检查缓存中是不是已经有方法实现了,有的话,直接调用。

3. 比较请求的选择器和类中定义的选择器,如果找到了,调用方法实现。

4.比较请求的选择器和父类中定义的选择器,然后是父类的父类,以此类推,如果找到了选择器,调用方法实现。

5. 调用resolveInstanceMethod:(或resolveClassMethod)。如果它返回YES,那么重新开始。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。

6. 调用forwardingTargetForSelector:,如果返回非nil,那就把消息发送到返回的对象上,这里不要返回self,否则会形成死循环的。

7.调用methodSignatureForSelector:,如果返回非nil,创建一个NSInvocation并传给forwardInvocation:。

8. 调用doesNotRecognizeSelector:,默认的实现是抛出异常。

先看下第5步,首先可以想到的就是用resolveInstanceMethod:和resolveClassMethod:在运行时提供实现,这通常是@dynamic合成属性的处理方式。简单来说,就是需要自己实现属性的getter和setter方法,通过resolveInstanceMethod:方法来把setter方法和getter方法和属性绑定在一起。

如果第5步返回NO的话,系统接着会首先尝试一次快速转发,也就是调用forwardingTargetForSelector:,看其能否返回一个对象,如果有对象返回,就转发给返回的对象。快速转发的原理其实就是先从缓存里找下是否存在对应的选择器。

如果快速转发返回nil的话,接下来就进行普通的转发,调用forwardInvocation进行普通的转发。

objc_msgSend还有几个相关的函数:objc_msgSend_fpret、objc_msgSendSuper、objc_msgSend_stret、objc_msgSendSuper_stret。SendSuper格式的函数很明显是把消息发送给父类,而带stret的在返回结构体时处理大部分情况。在Intel处理器上返回浮点数时,带fpret的函数处理大部分情况。

方法混写(Method Swizzling)与ISA混写(Isa Swizzling)

在Objective-C中,混写(Swizzling)是指透明地把一个东西换成另一个,我们可以利用Objective-C中的运行时来实现混写。我们先看下方法混写,Objective-C提供了以下API来动态替换类方法或实例方法的实现:

  • class_replaceMethod替换类方法的定义。

  • method_exchangeImplementations交换两个方法的实现。

  • method_setImplementation设置一个方法的实现。

我们来看下三者的区别:

  • class_replaceMethod,当需要替换的方法有可能不存在时,可以考虑使用该方法。

  • method_exchangeImplementations,当需要交换两个方法的实现时使用。

  • method_setImplementation是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

系统中提供的KVO使用到了isa混写,具体是怎么实现的呢?当你观察一个对象时,一个新的类会被自动创建,这个新类继承自该对象的原本的类,并且重写了被观察属性setter方法。重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值的更改。最后通过isa混写,把这个对象的isa指针指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例

注意一点:把isa指针指向新创建的子类,被观察的对象就变成了新创建子类的对象实例,这是由于isa指针永远指向其对应的类。用一张图来说明下:

键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:。在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变之后,didChangeValueForKey:会被调用,继而obserValueForKey:ofObject:change:context:也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们希望能控制回调的调用时机时才会这么做。

使用KVO的一个明显的优势就是零开销观察的优势,如果给定的实例没有观察者,那么KVO不会有任何消耗,因为根本没有KVO代码。而即使没有观察者,对于委托方法和NSNotification还得工作。

Runtime - 消息发送原理的更多相关文章

  1. runtime - 消息发送(objc_msgSend)

    http://www.jianshu.com/p/95c8cb186673 在OC中,我们对方法的调用都会被转换成内部的消息发送执行对objc_msgSend方法的调用,掌握好消息发送,可以让我们在编 ...

  2. ActiveMQ(2)---ActiveMQ原理分析之消息发送

    持久化消息和非持久化消息的发送策略 消息同步发送和异步发送 ActiveMQ支持同步.异步两种发送模式将消息发送到broker上.同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消 ...

  3. ActiveMQ消息的发送原理

    持久化消息和非持久化消息的发送策略:消息同步发送和异步发送 ActiveMQ支持同步.异步两种发送模式将消息发送到broker上.同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消 ...

  4. Objective-C 消息发送与转发机制原理(摘)

    八面玲珑的 objc_msgSend 此函数是消息发送必经之路,但只要一提 objc_msgSend,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP 并调用: id objc_msgSend( ...

  5. iOS 消息发送与转发详解

    Objective-C 是一门动态语言,它将很多静态语言在编译和链接时期做的事情,放到了运行时来处理.之所以能具备这种特性,离不开 Runtime 这个库.Runtime 很好的解决了如何在运行时期找 ...

  6. Objective-C中的消息发送总结

    关于OC中的消息发送的实现,在去年也看过一次,当时有点不太理解,但是今年再看却很容易理解. 我想这跟知识体系的构建有关,如果你不认识有砖.水泥等这些建筑的基本组成部分,那么我们应该很难理解建筑是怎么建 ...

  7. ActiveMQ点对点的消息发送案例

    公司最近会用MQ对某些业务进行处理,所以,这次我下载了apache-activemq-5.12.0-bin把玩下. 基于练习方便需要,使用Windows的版本. 参考的优秀文章: activemq的几 ...

  8. runtime——消息机制

    本文授权转载,作者:Sindri的小巢(简书) 从异常说起 我们都知道,在iOS中存在这么一个通用类类型id,它可以用来表示任何对象的类型 —— 这意味着我们使用id类型的对象调用任何一个方法,编译器 ...

  9. Unity 消息发送机制 解析

    该博客,只为解析,解析,解析,已经整理好,已经整理好,已经整理好.代码核心原理套用网上最流行的那一套,也是最常用游戏开发适用的消息机制.这里面加上自己的一些优化,极大的修正(哈哈),实测,没问题.万一 ...

随机推荐

  1. [BZOJ2095][Poi2010]Bridges 最大流(混合图欧拉回路)

    2095: [Poi2010]Bridges Time Limit: 10 Sec  Memory Limit: 259 MB Description YYD为了减肥,他来到了瘦海,这是一个巨大的海, ...

  2. libuv 简单使用

    libuv 简单使用 来源:https://zhuanlan.zhihu.com/p/50497450 前序:说说为啥要研究libuv,其实在很久之前(大概2年前吧)玩nodejs的时候就对这个核心库 ...

  3. 解决Android SDK Manager更新时出现问题

    使用SDK Manager更新时出现问题Failed to fetch URL https://dl-ssl.google.com/android/repository/repository-6.xm ...

  4. CSS的overflow属性介绍

    overflow 属性规定如何处理如何处理不符合元素框的内容.用法如下:Object.style.overflow=visible|hidden|scroll|auto. 参数介绍: visible: ...

  5. C++并发类成员函数调用(练习1)

    一般类成员函数开线程格式 std::thread t1(&类名::函数,&实例化对象,参数....) ||std::thread t1(std::bind(&&类名:: ...

  6. C++下实现同接口下多个类作为参数的调用和传参

    /* 实现同接口下不同类的对象的转移 定义类的接口 定义多个继承该接口的类 定义管理类,把接口当作类型, 传入该接口下各种类的对象,进行操作 */ #include<iostream> # ...

  7. bzoj 2002

    2002: [Hnoi2010]Bounce 弹飞绵羊 Time Limit: 10 Sec  Memory Limit: 259 MBSubmit: 12203  Solved: 6162[Subm ...

  8. python与pycharm

    什么叫自动化测试? 通俗来说,自动化测试就是通过写代码来帮我们测试软件.用来做自动化测试的语言很多,python,Java,php,Go,ruby等.而且软件系统开发语言与自动化测试语言可以不一致.例 ...

  9. enumerate 遍历numpy数组

    enumerate 遍历numpy数组 觉得有用的话,欢迎一起讨论相互学习~Follow Me 遍历一维数组 i,j 分别表示数组的 索引 和 存储的值 import numpy as np a=np ...

  10. [整理]SQL Server Reporting Services Charts

    http://msdn.microsoft.com/zh-cn/library/aa964128.aspx#mainSectionhttp://msdn.microsoft.com/en-us/lib ...