引言
OC是一门动态运行时语言,其方法调用其实就跟C++或者Java或其他面向对象语言中的方法调用差不多,只是形式有些不一样而已。而OC得方法调用的术语为消息,下文我以消息来称方法调用。下面将详细介绍为什么OC是动态的,以及编译器在不为人知的背后做了什么事情。
在深入了解消息之前,必须要先了解三个必备的概念:1.Class 2.SEL 3.IMP,它们分别在objc/objc.h中的定义:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
typedef struct objc_selector *SEL;
typedef id (*IMP)(id, SEL, ...);
Class的含义:
Class是一个指向 objc_class 的结构体指针,这个结构体标识每一个类的类结构。而 objc_class在 objc/objc_class.h 中定义如下:
struct objc_class {
struct objc_class * isa;
struct objc_class *super_class; /*父类*/
const char *name;/*类名字*/
long version;/*版本信息*/
long info;/*类信息*/
long instance_size;/*实例大小*/
struct objc_ivar_list *ivars;/*实例参数链表*/
struct objc_method_list **methodLists;/*方法链表*/
struct objc_cache *cache;/*方法缓存*/
struct objc_protocol_list *protocols;/*协议链表*/
};
所以 Class 是指向类的结构体的指针,这个类的结构体含有一个指向父类类结构体的指针,该类方法的链表,该类方法的缓存以及其他必要信息。
每一个类实例对象的第一个实例变量是一个指向该对象的类结构的指针,叫做 isa。通过 isa,对象可以访问它对应的类以及对应类的父类,如图所示:
我们知道 id 是一个指向 objc_object 结构体的指针,它是一个不确定类型,该结构体只有一个成员就是 isa ,所以任何继承 NSObject 的类的对象都可以用 id 来指代,因为 NSObject 的第一个成员实例就是 isa 。
方法的含义:
这一所说的方法链表存储的是 Method 类型,Method 在头文件 objc_class.h 中定义如下:
typedef struct objc_method *Method;
typedef struct objc_ method {
SEL method_name; //方法的名称
char *method_types; //参数类型
IMP method_imp; //具体实现的函数指针
};
一个方法 Method 包含了方法选表 SEL(表示方法的名称),一个 types(表示方法的参数类型),一个 IMP(指向该方法的具体实现的函数指针)。
SEL的含义:
在引言中我们看到了SEL(方法选标)的定义:
typedef struct objc_selector *SEL;
它是一个指向 objc_selector 指针,表示方法的名字。不同的类可以拥有相同的 selector,因为不同类的对象 performSelector 相同的 selector 时,会在各自的 selector (消息选标) 和 address (实现地址) 去查找 IMP (方法的实现),然后这个IMP执行具体的实现代码。这是一个动态绑定的过程,在编译的时候,我们并不知道最终执行哪一些代码,只有在运行的时候,通过selector 去查询方法名,才能确定具体执行什么代码以及代码的实现。
IMP的含义:
在引言中我们看到了IMP的定义为:
typedef id (*IMP)(id, SEL, ...);
IMP是一个函数指针,这个被指向的函数包含了一个接收消息的对象 id(self 指针),调用方法的选标 SEL(方法名),以及不定个数的方法参数,并返回一个 id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码。
方法调用过程:
我这里设计一个 Person 类的对象去调用方法eat:
Person *person = [[Person alloc] init];
[person eat];
对 eat 的调用,编译器会通过运行时动态插入一些代码,将其转化为对方法具体实现 IMP 的调用,而这个 IMP 是通过在 Person 的类结构中的方法链表中找到 eat 的方法选标 SEL 对应的具体方法实现。
上面提及编译器插入的一些代码是什么代码?下面展开这个话题。
消息函数obj_msgSend:
编译器会将方法转换为消息函数objc_msgSend的调用,这个函数主要有两个参数:1.消息接受者id 和 2.消息对应的方法选标 SEL,这两个参数是隐藏的无需提供给开发者看到。此外同时会接收到消息中的任意参数,这些参数是开发者对方法设计时所用到的参数:
id objc_msgSend(id theReceiver, SELtheSelector, ...)
如果我声明一个Person类,并调用eat方法 [Person eat]; 会被转化为如下形式的函数:
objc_msgSend(Person, @selector(eat));
消息函数 objc_msgSend 做了动态绑定所需要的一切工作:
1.消息函数首先找到 SEL(方法选标)对应的 IMP(方法实现);
2.然后将消息接收者对象以及方法中指定的参数传递给 IMP(方法实现);
3.最后将方法实现的返回值作为该函数的返回值来返回。
上述提到编译器方法调用的时候回自动插入的代码是该消息函数 objc_msgSend 的代码,我们不需要再代码中显示这个消息函数。当消息函数 objc_msgSend 找到对应的 IMP (方法实现)时,它直接调用这个方法的实现,并将消息中的所有参数都传递给方法实现,同时会传递两个隐藏的参数(1.消息接受者id 、 2.消息对应的方法选标 SEL)。这些参数帮主方法实现获得消息表达式的信息。在这两个隐藏参数中,self 更有用一些。实际上,self 是在方法实现中访问消息接收者对象的实例变量的途径。
查找 IMP 的过程:
前面说了,objc_msgSend 会根据方法选标 SEL 在类结构的方法列表中查找方法实现 IMP。这里面有文章,我们在前面的类结构中也看到有一个叫 objc_cache *cache 的成员,这个缓存为提高效率而存在的。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。
在查找IMP时:
1.首先去该类的方法 cache 中查找,如果找到了就返回它;
2.如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销。
3.如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class 指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的 IMP,返回它,并加入 cache中;
4.如果在自身以及所有父类的方法列表中都没有找到对应的IMP,则看是不是可以进行动态方法决议;
5.如果动态方法决议没能解决问题,进入消息转发。