在Object-C中,对象调用方法其实是对象接收消息,消息的发送采用"动态绑定"机制,具体会调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码。
OC调用方法的形式如下:
Person *p = Person.new;
[p eat];
从形式上看调用方法是使用中括号的形式,但是我们知道Object-C是动态语言,当代码执行到该调用的时候,系统到底做了哪些工作呢?底层到底怎么实现的呢?
先来看方法的定义,在objc/runtime.h
文件中:
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
根据源代码可以看出,方法Method
本质是一个objc_method
结构类型的指针,而结构体objc_method
中的成员分别为:
SEL
:方法名;char *
:方法的参数类型;IMP
:指向方法实现的函数的指针。
在文件objc/objc.h
文件中看SEL
的定义:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
根据上面源代码可知 SEL
本质是一个指向 objc_selector
结构类型的指针,表示方法的名字/签名。
在文件objc/objc.h
文件中看IMP
的定义:
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
根据上面源代码可知 IMP
是一个函数指针,这个被指向的函数包含一个接收消息的对象id
(self 指针),调用方法的选标 SEL
(方法名),以及不定个数的方法参数,并返回一个id
。也就是说 IMP
是消息最终调用的执行代码,是方法真正的实现代码 。
消息调用过程:
我们之前说过Class
被定义为一个指向 objc_class
的结构体指针,这个结构体表示每一个类的类结构:
struct objc_class {
// isa指针
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 父类指针
Class _Nullable super_class OBJC2_UNAVAILABLE;
// 类名称
const char * _Nonnull name OBJC2_UNAVAILABLE;
// 版本信息
long version OBJC2_UNAVAILABLE;
// 类信息
long info OBJC2_UNAVAILABLE;
// 实例大小
long instance_size OBJC2_UNAVAILABLE;
// 实例变量链表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 方法链表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 方法缓存
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 协议链表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
由此可见,Class
是指向类结构体的指针,该类结构体含有一个super_class
指针,它是指向该类的父类类结构的指针,一个该类方法的链表methodLists
,一个该类方法的缓存cache
以及其他信息。NSObject 的 class 方法就返回这样一个指向其类结构的指针。每一个类实例对象的第一个实例变量是一个指向该对象的类结构的指针,叫做isa。
消息传递框架:
我们知道对象的本质是指向objc_object
结构类型的指针,它的第一个成员就是isa
。如上图中圆形所代表的就实例对象,它的第一个实例变量为 isa,它指向该类的类结构 The object’s class。而该类结构有一个指向其父类类结构的指针superclass, 以及自身消息名称(selector)/实现地址(address)的方法链表;其实在该类结构中还有一个isa指针,指向的是该类的元类。
再来说刚才的问题,消息调用的过程,当代码执行到[p eat];
时,编译器通过插入一些代码,将之转换为对方法具体实现IMP的调用,这个IMP
是通过在 Person
的类结构中的方法链表中查找名称为 eat
的 选标 SEL
对应的具体方法实现找到的。
那么编译器插入了什么代码呢?首先编译器会将消息转换为对消息函数 objc_msgSend()
的调用:
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
- 参数
self
:指向要接收消息的类的实例的指针,如果是实例对象则为实例对象方法(减号方法),如果是类对象则为类方法(加号方法); - 参数
op
:消息对应方法的选标; - 接收消息中的任意参数;
id
类型的返回值。
消息[p eat]
会被转换为如下方式的函数调用:
objc_msgSend(p, @selector(eat));
// 类方法
// objc_msgSend(p.class, @selector(eat));
该消息函数做了动态绑定所需要的一切工作:
1,它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接收者的类型。
2, 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
3, 最后,将方法实现的返回值作为该函数的返回值返回。
编译器会自动插入调用该消息函数objc_msgSend的代码,我们无须在代码中显示调用该消息函数。当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:消息的接收者以及方法名称 SEL。这些参数帮助方法实现获得了消息表达式的信息。它们被认为是”隐藏“的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。
尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就象可以引用消息接收者对象的实例变量一样)。在方法中可以通过self来引用消息接收者对象,通过选标_cmd来引用方法本身。在下面的例子中,_cmd 指的是eat方法,self指的收到eat消息的对象。在这两个参数中,self更有用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
查找 IMP 的过程:
前面说了,objc_msgSend 会根据方法选标 SEL 在类结构的方法列表中查找方法实现IMP。这里头有一些文章,我们在前面的类结构中也看到有一个叫objc_cache *cache 的成员,这个缓存为提高效率而存在的。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。。
下面来剖析一段苹果官方运行时源码:
static Method look_up_method(Class cls, SEL sel,
BOOL withCache, BOOL withResolver)
{
Method meth = NULL;
if (withCache) {
meth = _cache_getMethod(cls, sel, &_objc_msgForward_internal);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return NULL;
}
}
if (!meth) meth = _class_getMethod(cls, sel);
if (!meth && withResolver) meth = _class_resolveMethod(cls, sel);
return meth;
}
通过分析上面的代码,可以看到,查找时:
1、首先去该类的方法 cache中查找,如果找到了就返回它;
2、如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销;
3、如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的IMP,返回它,并加入cache中;
4、如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则看是不是可以进行动态方法决议(后面有专文讲述这个话题);
5、如果动态方法决议没能解决问题,进入下面要讲的消息转发流程。
参考:
1、[Cocoa]深入浅出 Cocoa 之消息;
2、消息发送官方文档