一、引言
Objective-C是一种很优美的语言,至少在我使用其进行编程的过程中,是很享受他那近乎自然语言的函数命名、灵活多样的方法调用方式以及配合IDE流顺畅快编写体验。Objective-C是扩展与C面向对象的编程语言,然而其方法的调用方式又和大多面向对象语言大有不同,其采用的是消息传递、转发的方式进行方法的调用。因此在Objective-C中对象的真正行为往往是在运行时确定而非在编译时确定,所以Objective-C又被称为是一种运行时的动态语言。
本篇博客既不介绍iOS开发,也不提及MacOS开发,只对Objective-C语言的这种消息机制与运行时动态进行探讨,所提及的内容也都是我开发中的个人积累与经验,如果偏颇之处,欢迎讨论指正。
二、消息发送与转发机制
1.初窥消息发送机制
许多面向对象语言中方法的调用都是采用obj.function这样的方式,在Objective-C语言中却是采用中括号包裹的方式进行方法调用,例如[obj function]。实际上,Objective-C中的每一句方法调用最后都会转换成一条消息进行发送。一条消息包含3部分内容:方法选择器、接收消息的对象以及参数。objc_msgSend函数就是用来发送这种消息。例如,创建一个Xcode命令行工程,我们创建一个类,命名为MyObject,如下:
MyObject.h文件:
#import <Foundation/Foundation.h>
@interface MyObject : NSObject
@end
MyObject.m文件:
#import "MyObject.h"
@implementation MyObject
-(void)showSelf{
NSLog(@"MyObject");
}
@end
首先在MyObject.h文件中并没有暴漏任何方法,MyObject.m文件中添加了一个showSelf方法,这个方法只是做了简单的打印操作。
将main.m文件修改如下:
#import <Foundation/Foundation.h>
#import "MyObject.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject * obj = [[MyObject alloc]init];
[obj class];
//为了消除未定义选择器的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//进行消息发送
((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
}
return 0;
}
运行工程,可以看到控制台执行了MyObject类的示例方法showSelf。如果要进行传参,在objc_msgSend方法中继续添加参数,并且指定对应的函数类型即可,例如:
MyObject.m文件:
#import "MyObject.h"
@implementation MyObject
-(void)showSelf:(NSString*)name age:(int)age{
NSLog(@"MyObject:%@,%d",name,age);
}
@end
main.m文件:
#import <Foundation/Foundation.h>
#import "MyObject.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject * obj = [[MyObject alloc]init];
[obj class];
//为了消除未定义选择器的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//进行消息发送
((void(*)(id,SEL,NSString*,int))objc_msgSend)(obj,@selector(showSelf:age:),@"珲少",25);
#pragma clang diagnostic pop
}
return 0;
}
运行工程可以看到方法被调用,参数被正确传入。
2.消息传递是基于继承链的
上面代码只是简单演示了消息发送的效果,下面我们来剖析下消息发送的过程与原理,明白了这个原理,对Objective-C中许多神奇的现象你将会豁然开朗,后面我会再具体向你介绍这些现象。
在介绍消息机制之前,我还是要再啰嗦一点,关于@selector()我们还需要深入理解一下,通过@selector(方法名)可以获取到一个SEL类型的对象,SEL实际上是objc_selector结构体指针,在Objective-C库头文件中没有找到objc_selector结构体的定义,但我们可以合理猜测,其中很有可能包含的是一个函数指针。因此SEL也可以理解为函数签名,在程序的编译阶段,我们定义类中所有所发会生成一个方法签名列表,这个列表时类直接关联的(原则上来说,类的本质也是对象,它是一个单例对象),在运行时通过方法签名表来找到具体要执行的函数。
我们再来看objc_msgSend()函数,前面说过,它的第一个参数为接收消息的对象,第2个参数为方法签名,之后为传递的参数。那么Objective-C运行时是如何根据一个对象实例来找到方法签名表,再找到要执行的方法呢,看似麻烦的事情其实原理也非常简单,细心观察,你会发现所有的NSObject子类对象中都包含一个isa成员变量,请看NSObject类的定义:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
这个isa变量是Class类型,我们的主角终于来了,Class顾名思义就是“类”类型,其实质是objc_class结构体指针:
typedef struct objc_class *Class;
有些蒙圈了吧,不用着急,拨开层层迷雾,你就会发现Objective-C中类本质上只是结构体而已,下面是objc_class结构体的定义:
struct objc_class {
//元类指针
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//父类
Class super_class OBJC2_UNAVAILABLE;
//类名
const char *name OBJC2_UNAVAILABLE;
//类的版本
long version OBJC2_UNAVAILABLE;
//信息
long info OBJC2_UNAVAILABLE;
//内存布局
long instance_size OBJC2_UNAVAILABLE;
//变量列表
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
//函数列表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
//缓存方式
struct objc_cache *cache OBJC2_UNAVAILABLE;
//协议列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
每一个“类”对象是也有一个isa指针,这个指针指向的类实际上是元类,即构造“类”的类。现在你无须纠结这些概念,举一个例子你就能明白,在Objective-C开发中有加方法与减方法,减方法是实例对象调用的方法,每一个“类”中都包含一个函数列表,就是上面的objc_method_list结构体数组指针,同样如果调用加方法,实际上是从类的元类中找到对应的方法列表,这个列表就是我们前面提到的方法签名列表,进行方法的执行。关于实例对象,“类”对象和元类,下图很好的表现了他们之间的关系:
需要注意,使用LLDB调试器我们是可以拿到对象的isa指针的,并且可以看出它的确为Class类型,但是我们缺无法通过isa指针继续向下取抓取更多类的信息,其所在的内存是禁止我们访问的。但是Objective-C运行时提供了一些方法可以获取到这些信息,后面我们会一一介绍。
上面我们介绍的消息发送机制其实十分不完整,首先Objective-C是支持继承的,因此如果在当前对象的类的方法列表中没有找到此消息对应的方法签名,系统会通过super_class一层层继续向上,直到找到相应的方法或者到达继承链的顶端。
有了上面的理论知识作为基础,我们就可以更深入的分析消息传递的过程了,首先,如果消息的接收对象刚好可以处理这个消息,即其isa指针对应的类中可以查找到这个方法,那么万事大吉,找到对应方法直接执行就大功告成,可以如果接收对象无法处理,其父类,父父类...等都无法处理,那么该怎么办呢,Objective-C为了增强语言的动态性,如果真的出现了这种情况,程序并不会马上crash,在crash前,有3次机会可以挽救本条消息的命运。