前言
Objective-C 是一门动态语言,在它里面有一个叫运行系统的东西,称之为runtime。runtime它究竟长什么样呢?为什么Objective-C调用对象的方法称为传递消息
呢?本文将在下面回答这两个问题。
理解消息传递
由于Objective-C是C的超集,所以我们先了解一下C语言的函数调用方式。C语言使用静态绑定
,也就是说在编译完成后,就决定了运行时候所调用的函数,代码如下:
1 |
|
从面可以看到,在编译的时候已经知道程序中printHello和printGoodBye这两个函数了,就会直接生成调用这些函数的指令。而这个函数的地址也是在指令中的。C语言还一种叫动态绑定
,代码如下:
1 |
|
如上所示,调用的函数要在运行期才能确定,在这种情况下生成的指令跟第一个例子不太一样。在第一个例子中,if和else都是函数的调用指令,而在第二个例子里,只有一个函数指令fnc()
,这个指令所调用的函数无法硬编码在指令中,而是要在运行中取出来。
在Objective-C中,如果向某个对象传递消息,就会使用动态绑定来决定调用方法。在底层所有的方法都是普通的C语言函数,在对象收到消息后,对象会搜索它属的类里面的方法列表
。找到相应用的方法,则调用该方法,如果找不到,继续找它的父类或者祖父,直到找到为止。
在最后确实是找不到就执行消息转发
(下面会讲到)。
在Objective-C中,下面的这个方法
1 | [receiver message] |
会被编译成
1 | objc_msgSend(receiver, @selector(message)) |
如下图
类结构
Class
打开NSObject.h文文件中,可以看到
1 | @interface NSObject <NSObject> { |
NSObject
里面有个isa
变量,类型为Class,再进入objc.h文件头文件中,查看Class
是如何定义的。
1 | typedef struct objc_class *Class; |
最终我们看到Class
是结构体objc_class
1 | struct objc_class { |
我们来分析一下上面变量:
- isa 在Objective中,类本身也是一个对象,这个对象里有个isa的指针,指向metaClass(元类)
- super_class 父类的Class
- ivars 变量
- methodLists 方法链表,
- cache 缓存着最近调用的方法
- objc_protocol_list 协议链表
1 | /// Represents an instance of a class. |
isa 与 元类(Meta Class)
上面提到在类本身也是一个对象,所以我们可以向对象发送消息,即调用类方法
例如:
1 | NSArray *array = [NSArray array]; |
NSArray接收到+array这个消息,接着从类对象的方法列表找到类方法。那个问题来了,这个类对象就是isa指向包含这些类方法的对象(结构体),而这个类称之为元类。
多说一句,元类存放着一个类里面的类方法。
下面可以拿出这张神图:
- 从上面可以看到,所有的meteClass的isa指针都指向RootClass,而RootClass则指向自身,在我们普通的NSObject体系中,RootClass即为我们的NSObject.
- 当我们调用对象的方法时,它会在自身的isa指向的类中查找方法,如果找不到则会通过class的super_class查找父类的类对象,查找里面的方法,一直查到为止,直到根部的class。如果实在是查不到则会进行
消息转发
- 当我们调用类方法时,它会通过自身的meteClass查询meteClass所持有的类方法,同样找不到的时候会向上层的父类查找方法。一直查到RootClass。
所以类自身也是一个对象,我们可以向类本身发送消息。
这个例子中,+array消息发送给NSArray类,而NSArra也是一个对象。
Super Class
讲到这个,不得不提一下sunny大神的一道面试题!
1 | @implementation Son : Father |
从上面我们可以知道[self selector]
会被编译成为objc_msgSend(self, selector)
,self
会被当为一个参数,而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。可以看super的定义:
1 | struct objc_super { id receiver; Class superClass; }; |
当调用[super class]
会编译成为
1 | id objc_msgSendSuper(struct objc_super *super,@selector(class), ...) |
注意一下上面的第一个参数是objc_super
结构体,它里的recevier指向的还是self
。该函数会从objc_super
中的superClass
的列表中找到方法,然后发送到接收者recevier,也就是self
。所以也就是相当于下面调用的方式:
1 | objc_msgSend(objc_super->receiver, @selector(class)) |
因为receiver是self,最后跟
1 | objc_msgSend(self,@selector()) |
回到题目中的self
是Son
类,所以两个输出都是Son。
成员变量(Ivar)
1 | struct objc_ivar { |
方法与缓存
方法的定义如下:
1 | struct objc_method { |
下面来详细说一下方法里面的变量是什么?
SEL
1 | typedef struct objc_selector *SEL; |
在方法里面定义了SEL
的指针,我们对SEL
很熟悉,它本质上来说是一个方法的KEY,它具有唯一性。
IMP
1 | id (*IMP)(id, SEL, ...) |
IMP是实际是函数指针,前面提到SEL
是方法的唯一性,因此发送消息的时候,我们可以根据SEL
快速找到对应的IMP
。
缓存
在上面,我们看到objc_cache
这个类型,顾名思义,它是一个缓存,是用来缓存方法的。
1 | struct objc_cache { |
Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。