OC消息发送机制完整全过程

前言

时间荏苒,光阴似箭啊,不知不觉,自己从接触iOS开发至今已经六年有余了。想想最开始学习OC时,苹果还未推出ARC机制,iOS/Mac开发也只有OC这么一门语言,swift还未推出。仔细想想自己从接触到工作这么多年来,自己接触到的OC语言相关问题的坑还是蛮多的,今天就让我给大家好好的讲讲iOS面试中甚至开发中经常接触到的一个东西:消息发送机制

正文

我从学习编程开发到现在也是学过和接触过很多编程语言了,大家熟知的汇编/C/C++/swift,我都一一认真的学过,因此对编程语言也是略知一二。不过呢,在这些常见的编程语言中,OC也算是比较独特的一门语言了,尤其是它的动态性,当然还有它那反人类的方法调用语法-中括号。说到方法调用,就不得不提到的就是OC里的另一个独特点,那就是消息发送机制,这也可以说是在其他大部分编程语言中没有的东西。下面就让我开始表演吧。

大部分语言的方法调用

说起大部分的语言的方法调用这个东西,可能就有点笼统了,确切的说呢,方法调用其实都是这一个样子的,哪个样子的呢,就是无论啥语言最终转成的汇编代码里,方法调用都是一个样子,那就是直接或间接的call某个地址,而这里的某个地址,就是函数实现的地址,直白的说就是那个函数实现的那段代码的入口首地址。

类对象、元类对象

其实呢,OC的方法调用最终也是上面这种形式的,只是呢,它在进入最终的这个调用形式的之前,四处瞎转悠了一段时间,转悠完后才进入这最终步骤的。而他这四处转悠那段时间干的事情就是消息发送了。

说起OC的消息发送就不得不说起的一个东西,那就是类对象和元类对象了。

不知道大家是否曾想过这样一个问题,一个对象的类信息是存储在哪儿的?所谓的类信息,就是类的属性名、方法等这些东西。其实可能有挺多小伙伴应该知道一个东西,那就是,一个对象的内存布局,几乎大部分面相对象编程语言中,一个对象具体的属性值(例如,a = 10,那么10就是这个具体的属性值)是直接存储在相应的对象的内存布局中的。而对象的属性名、方法这些东西是不会存储在一个对象的内存布局中的,原因其实很简单的。首先我们来思考一个问题,为何一个对象要将具体的属性值存储在自己的内存布局中呢?如果大家仔细思考过这个问题的话,会发现,不同的一个实例对象,在内存中都有独一无二的一块内存存储着它,这就说明一个问题,不同的内存中存储着不同的一个对象,也就是,某一块内存中存储着的对象都是不同于其它任何内存中存储的对象。既然如此,那就意味着不同的对象的各个属性值都应该是自己独有的,自己可以进行相应的修改,并且改了不会影响其它任何内存中存储着的实例对象。总结一句话就是,因为每个对象都要拥有自己的各个属性值,因此最好的方式就是在自己的内存布局之中存放这些具体的属性值(P.S. 其实这里也给大家解释了OC里另一个知识点,那就是,为何不能在分类中添加成员变量了,原因就是添加成员变量会直接影响到一个对象的内存布局,所以并不是苹果没写分类中添加成员变量这样的功能,而是他们压根实现不了这功能,就是因为载进内存里的程序的内存布局是不允许修改的,也不可能修改的)。那么,想必大家对属性名、方法这些东西为何不存储在对象的内存布局中有些端倪了吧,就像上面说的,原因很简单,那就是一个类的任何对象的属性名、方法这些东西都是一样的,既然都是一样的,那么又何必大费周章的在每个实例对象中都存储一遍呢?因此,苹果就将这些都一样的东西放在了另一个东西里面了,那就是这里要提到的类对象和元类对象。类对象里存储着一个类的实例属性、实例方法等,元类对象存储着一个类的类方法等。其实呢,实例属性、实例方法、类方法这些东西可以放在同一个东西里面,苹果为何分成两个东西来存储,我想应该是为了方便管理和区分。这里其实也可以知道一点,那就是,一个程序中,一个类的实例对象可以有无数个,但是这些实例对象对应着的类对象和元类对象有且仅有一个,原因想必大家一目了然,就是因为一个程序中,一个类有且只有一个,那么就自然而然只有一个类对象和元类对象来存储这些类信息。

这里给上面做个大体的总结:一个类的类信息(也就是属性和方法这些一个类都共同具有的东西)是需要一个东西来存储的,那就是类对象和元类对象,类对象存储一个类的实例方法、实例属性之类的实例相关的东西,元类对象是用来存储一个类的类方法等类性质相关的东西的。而一个对象的具体数值都是存储在自己的内存布局中的,原因就是每个对象都要独立拥有自己的属性值,可以的对这些属性值进行修改并不影响到其他对象。

类对象和元类对象中存储的东西

类对象和元类对象中存储的东西很多,我这里就不一一赘述了,我在这里就只说我们这篇文章应该用到的东西。首先,就是大家耳熟能详的一个东西isa指针,isa指针是实例对象、类对象、元类对象都有的一个东西。实例对象的isa指向着类对象,而这个类对象中存储着类实例对象的相关信息,可以告诉实例对象,你是个什么类,你有哪些属性、方法,遵守了哪些协议等。类对象的isa指针指向了元类对象,元类对象中存储着一个类的类方法等类性质相关的东西。元类的isa指向NSObject的元类对象。如下图

而类对象和元类对象中都存储着一个superclasss,用于存储自己相应的父类,并可以通过这个superclass查找到自己的父类。例如Animal继承自NSObject,Dog继承自Animal,那么指向关系就是,Dog的实例对象的isa指向Dog的类对象,Dog的类对象的isa指向Dog的元类对象。Dog的类对象的superclass指向着Animal的类对象,Dog的元类对象的superclass指向Animal的元类对象。类对象的superclass一层一层往上指向父类的类对象,直到NSObject的类对象,NSObject的类对象的superclass指向nil。元类对象的superclass一层一层往上指向父类的元类对象,直到NSObject的元类对象,NSObject的元类对象的superclass指向NSObject的类对象。关系图如下,这里的实线箭头是superclass,虚线箭头是isa

消息发送机制

消息发送机制总结起来分为三个大的过程:

1、消息发送过程(也就是方法查找过程)

2、动态方法解析过程

3、消息转发过程

1、消息发送过程

首先得说一个前提条件,那就是,如果能用中括号调用方法的话,那么这个方法一定是有声明的,这个可能有些小伙伴并不会注意到这一点,那就是,正常情况下,一个方法都是有声明和实现的,声明就是告诉你有这个方法存在,实现就是这个方法的具体实现,

说起消息发送过程,就得说说,在类对象和元类对象中,有两个东西,一个是方法缓存列表,也即是cache,这是个散列表数据结构(也就是大家耳熟能详的哈希表),它主要用于缓存自己之前调用过的方法。另一个是方法列表,这里面存储着一个类的所有方法,包括分类添加的方法。注意:类对象和元类对象都有这两个东西,只是一个存储着实例方法,一个存储着类方法。

下面就是消息发送的详细过程(这里我们只说实例方法,类方法是一样的,只是最终在元类对象中查找的而已):

首先,这也是能解决小伙伴们很多时候遇到的某些莫名问题的原因,那就是,在消息发送的最最开始的地方,会检查你调用的方法的对象(也就是self)是不是一个nil,如果是的话,就直接停止后面所有的操作,原因很简单,你都穿了个空对象过来,后面的所有操作其实都是没有任何意义的啦。紧接着,就会通过实例对象的isa指针找到其类对象,之后再类对象的cache中进行查找,如果找到,就直接调用,之后结束查找。

如果在cache中没找到,则在这个类对象的方法列表中进行查找,如果找到,则调用,并将此方法存放在自己的cache中,之后结束查找。

如果在类对象中未找到,怎通过类对象的superclass找到父类的类对象,之后重复前两步(也就是先在cache中查找,之后再在方法列表中查找)。superclass为nil,如果superclass为nil,则进入下面的动态方法解析阶段。注意:这里找到之后会将找到的方法放在自己的cache中,而不是父类或者父类的父类的cache中,并且,即使是在父类的cache中找到的话,也会放在自己的cashe中的。这样的原因很简单:尽量的减少方法查找的流程,一步到位,这样就可以节省很多时间

具体如下图:


消息发送过程图

2、动态方法解析阶段

进入这里之前,首先会判断是否进入过这个动态方法解析阶段,如果进入过则直接进入下一步:方法转发阶段。如果没有进入过,就会进入这个方法解析阶段,方法解析会调用一个法法,叫做- resolveInstanceMethond: ,类方法的动态方法解析阶段的话就叫做+ resolveInstanceMethond:,我们前面已经说过了,一个方法能用中括号进行调用的话,那么这个方法必须至少有个声明,但是呢,如果能来到这儿,也就证明了一点,这个方法只有声明,没有实现,而这个动态方法解析的resolveInstanceMethond: 方法,就是给你调用的这个方法添加方法实现的,也就是具体的方法实现代码。

当进入动态方法解析时,会调用resolveInstanceMethond:这个方法,执行方法里的代码,而正常的操作都是会在这个方法里进行动态添加方法实现代码,怎么添加,大家可以自己搜索。之后将这个动态方法解析阶段标记为已经执行过的状态。最后重新回到第一阶段的消息发送阶段----也就是在cache和方法列表中查找方法,为何要重新进入第一阶段呢,原因很简单,因为你已经为这个方法添加了实现,说白了就是将方法的实现添加进了那个相应的类的方法列表里了,如果你已经在动态方法解析阶段添加了方法实现的话,那么重新进入第一阶段的时候,就能够在那个类的方法列表中找到那个方法的实现了。如果你没有添加方法实现的话,重新进入第一阶段的时候,就找不到方法实现,那么再次重新进入动态方法解析阶段的时候,由于已经标记为执行过动态方法解析了,就不会再进入动态方法解析,就会直接进入下一个阶段,那就是最后一个阶段,消息转发阶段

具体如下图:


动态方法解析过程图

3、消息转发阶段

消息转发,可谓是高级iOS面试问得最多的问题之一了吧,然鹅实际开发应用场景却是不多的。

进入消息转发阶段后,会首先调用这个方法forwardingTargetForSelector:,这个方法会返回一个实例对象值,如果返回的是nil,则进入下个步骤,调用methodSignatureForSelector:。如果返回的是一个具体的实例对象,那么就会直接在这个实例对象的cache和方法列表中查找这个方法,找到的话就调用,没找到的话,就方法找不到的那个错误。注意,这里调用返回的这个实例对象的方法名字和最开始查找的那个方法名字是一毛一样的。

进入methodSignatureForSelector:,这个方法调用完,会返回一个methodSignature,如果返回的是空,则直接报方法找不到的错,如果返回的不为空,则进入forwardInvocation:这个方法,注意,到这儿了, 就会直接调用这个方法,言下之意就是,你在这个方法里写了什么代码,就会一五一十的执行里面的代码,所以爱搞事的其实就可以在这里面写很多搞事的代码啦,哈哈哈。当然,常规操作是,会根据methodSignatureForSelector:方法返回的methodSignature,之后调用这个方法签名指向的方法实现。请注意:由于这里是直接调用这个forwardInvocation:方法,因此,其实也可以直接不管methodSignatureForSelector:返回值的东西,只要你明确知道自己想干什么操作,就可以直接在这forwardInvocation:方法里把你想写的操作写进去。

具体如下图:


消息转发过程图

总结

到此为止,完整的消息发送机制就已经讲完了。希望能对你的工作和面试有所帮助。


附加知识NSProxy

其实有时候,可能会存在这样一种需求,那就是,不执行消息发送的第一阶段和第二阶段,能够直接一上来就直接进入消息转发阶段。这个其实苹果早已为我们想到了,那就是你自定义一个类继承自NSProxy,这个类和NSObject一样,是属于OC少有的基类,通过这个类,很特殊,特殊在哪儿呢,你会发现,生成这个类的一个对象时,只有一个alloc方法,并没有init方法,是不是感觉刷新了你对OC语言的认知新高度呢?当然它还有一个特殊点,就在于,一旦你实现了第三阶段的三个方法的时候,调用方法时,就会略过第一阶段和第二阶段,直接进入第三阶段。这样的话,就给消息转发提供了很大的一个捷径。

大家可能会纳闷,苹果怎么要专门搞这个一个类呢,而且主要作用还是这样的。这个原因的话,就得说到多继承了。大家都知道,现在开发中流行的很多开发语言都是单继承语言,就是因为多继承容易出现一系列麻烦的问题,一个是菱形继承的问题,一个是继承逻辑的问题,注意,多继承下是很容易出现这两种情况的,尤其是继承逻辑问题。这也是为啥如今很多面向对象语言都是单继承的原因。然鹅呢,多继承也是有它的优势之处的,例如本来某各类就同时拥有另外两个类的特性,那样的话,同时继承自这两个类是一种很好的方式。

因此,大部分单继承语言中,为了能保留多继承的优势,就另辟蹊径的实现了多继承,OC中当然也有这个另辟蹊径的方法,那其中一种方法就是这里提到的消息转发。大家可以想想,为啥第三阶段的名字非得叫做消息转发呢,听名字就能让你一下子明白这个阶段的主要作用了,那就是将本身类没有实现的方法转发到另一个类的方法实现上。举个例子,类A有test1和test2的方法实现,类B有test3和test4的实现,并且类A和B并无任何的继承关系,并且A已经有一个父类了,那么现在想在类A中声明test3和test4,并且实现的方法功能也是和类B的test3和test4一毛一样,并且能直接通过类A的对象直接调用,那么,最好的方法就是直接调用类B的这两个方法,那么这不就是多继承了吗。而这个例子,能够用消息转发轻易的实现,而NSProxy刚好又是那个一步到位直接进入消息转发阶段的东西,因此这就是NSProxy出现的主要原因之一。

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

推荐阅读更多精彩内容