Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。
消息传递机制
Alan Kay 曾多次强调 Smalltalk 的核心不是面向对象,面向对象只是the lesser ideas,消息传递才是the big idea
在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。而在 Objective-C 中,[object foo]语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。
事实上,在编译时你写的 Objective-C 函数调用的语法都会被翻译成一个 C 的函数调用 -objc_msgSend()。比如,下面两行代码就是等价的:
[array insertObject:foo atIndex:5];
objc_msgSend(array,@selector(insertObject:atIndex:),foo,5);
消息传递的关键藏于objc_object中的 isa 指针和objc_class中的 class dispatch table。调度表。
在Objective-C中,类、对象和方法都是一个C的结构体,从objc/objc.h,头文件中。
isa指针:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if!__OBJC2__Class super_class;
constchar*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;
#endif};
struct objc_method_list{
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char*method_types;/* a string representing argument/return types */
IMP method_imp;
};
objc_method_list 本质是一个有objc_method 元素的可变长度数组。一个 objc_method机构体中有函数名,也就是SEL,有表示函数类型的字符串,以及函数的实现IMP。
IMP:IMP是”implementation”的缩写,它是objetive-C 方法(method)实现代码块的地址,可像C函数一样直接调用。通常情况下我们是通过[object method:parameter]或objc_msgSend()的方式向对象发送消息,然后Objective-C运行时(Objective-C runtime)寻找匹配此消息的IMP,然后调用它;但有些时候我们希望获取到IMP进行直接调用。
由于Method的内部结构不可见,所以不能通过method->method_name的方式访问其内部属性,只能Objective-C运行时提供的函数获取。
SEL method_getName(Method method);
IMP method_getImplementation(Method method);
const char * ivar_getTypeEncoding(Ivar ivar);
从这些定义中可以看出发送一条消息也就是objc_msgSend做了什么,例如:objc_msgsend(obj,foo):
1、首先,通过obj的isa找到它的class;
2、在class的method list 中找到 foo;
3、如果class中没有foo,继续往它的superClass中找;
4、一旦找到foo函数,就去执行他的IMP。
但是这种实现又个问题就是效率低,一个class中往往只有20%的函数会经常被调用,可能占总调用次数的80%。每个消息都要遍历一边,objc_method_list并不合理。如果把经常被调用的函数缓存下来,那就可以大大提升函数查询的效率。这就是objc_class 中另一个重要成员objc_cache做的事情。在找到foo之后,把method_name作为key,method_IMP作为value给存起来。当再次收到foo消息时候,可以直接在cache中找到,避免去遍历objc_method_list;
动态方法解析和转发
在上面的例子中,如果foo没有找到会发生什么?通常情况下,程序会在运行时crash,并抛出,unrecognized selector sent to ..异常,但是在抛出异常前,Object-c的运行时会给你三次拯救程序的机会:
1、Method resolution
2、Fast forwarding
3、Normal forwarding
Method resolution
首先Object-C运行时会调用 +resolveInstanceMehtod:或者+resolveClassMethod:,让你有机会提供一个函数的实现。如果你添加了函数并返回YES,那么运行时就会重新启动一次消息发送的过程。还是以foo为例,你可以这么实现:
void fooMethod(idobj, SEL _cmd){
NSLog(@"Doing foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL{
if(aSEL ==@selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod,"v@:");
returnYES;
}
return [super resolveInstanceMethod];
}
PS:iOS4.3 加入很多新的runtime方法,主要都是以imp为前缀的方法,比如:imp_implementationWithBlock() 用block 快速创建一个imp。上面的例子可以重写成:
IMP fooIMP = imp_implementationWithBlack( ^(id _self)){
NSLog(@"Doing foo");
});
class_addMehtod([self class],aSEL,fooIMP,"v@:");
Core Data 有用到这个方法。NSManageObjects 中的properties 的getter和setter 就是在运行时动态添加的。
Message Forwarding
如果resolve方法饭回NO,运行时就会移到下一步:消息转发(Message Forwarding)
FastForwarding
如果目标对象实现了 -fowardingTargetForSelector:,Runtime这时候就会调用这个方法,给你一个这个消息转发给其他对象的机会。
- (id)forwardingTargetForSelector:(SEL)aSelector{
if(aSelector ==@selector(foo:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Forwarding。这里叫Fast,只是为了区别下一步转发机制。因为这一步不会创建任何心的对象,但下一步转发会创建一个NSInvocatioin对象,所以相对更快一些。
Normal Forwarding
这一步是 Runtime 最后一次给你挽救的机会。首先它会发送
- methodSignatureForSelector:消息获得函数的参数和返回值类型。如果返回 nil,Runtime 则会发出
- doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送 -fowardInvocation:消息给目标对象。
NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。所以你可以在- forwardInvocation:里修改传进来的NSInvocation对象
-(void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = invocation.selector;
if (alternateObject respondsToSelector:sel){
[invocation invokeWithTarget:alternateObject]
}else{
[self doesNotRecognizeSelector:sel];
}
}
总结
Objective-C 中给一个对象发送消息会经过以下几个步骤
1、在对象类的dispatch table中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
2、如果没有找到,Runtime 会发送+resolveInstanceMethod:或者+resolveClassMethod:尝试去 resolve 这个消息
3、如果 resolve 方法返回 NO,Runtime 就发送-forwardingTargetForSelector:允许你把这个消息转发给另一个对象。
4、如果没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector:和-forwardInvocation:消息。你可以发送-invokeWithTarget:消息来手动转发消息或者发送-doesNotRecognizeSelector:抛出异常
参考了:http://tech.glowing.com/cn/objective-c-runtime/