Objective-C 运行时
用C和汇编写的可以实现同其他语言交互的动态共享库。位置:/usr/lib/libobjc.A.dylib.
Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是:
通过 Objective-C 源代码;
通过 Foundation 框架的NSObject类定义的方法;例如:
- const char * NSGetSizeAndAlignment ( const char *typePtr, NSUInteger *sizep, NSUInteger *alignp );
- Class NSClassFromString ( NSString *aClassName );
- NSString * NSStringFromClass ( Class aClass );
- SEL NSSelectorFromString ( NSString *aSelectorName );
- NSString * NSStringFromSelector ( SEL aSelector );
- NSString * NSStringFromProtocol ( Protocol *proto );
- Protocol * NSProtocolFromString ( NSString *namestr );
通过对 runtime 函数的直接调用。徐引入头文件#include
Objective-C源代码
大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义。
NSObject的方法
Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
Runtime的函数
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。
刨根问底Objective-C Runtime(1)- Self & Super
所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。
而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。第二步是去 Father这个类里去找- (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用,此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。
刨根问底Objective-C Runtime(2)- Object & Class & Meta Class ###
刨根问底Objective-C Runtime(3)- 消息 和 Category
一个类的多个类别同时被加入到工程中时,只需引入一个类别的头文件。
一个类的多个类别中如果有重名函数,按照compile source中的编译顺序压栈加入到类的函数列表中,最后一个被编译的函数被调用。
刨根问底Objective-C Runtime(4)- 成员变量与属性
objc_msgSend 函数和消息转发
objc_msgSend 函数
objc_msgSend 函数
objc_msgSend函数负责像某对象发送一个消息。定义如下
id objc_msgSend(id self, SEL op, …)
在OC里面我们调用对象方法[Receiver message]的这种模式,实际是通过调用objc_msgSend(Receiver,message,…)函数来找到方法的实现入口。objc_msgSend实现原理是通过对象对应的objc_object的ISA找到该类对应的objc_class结构体。通过依次遍历objc_cache,objc_method_list里面的方法找到方法实际入口,如果没有找到,则跳到父类寻找,以此类推如果最终都没有找到就会发生下面介绍的消息转发机制。
消息转发机制
Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提 升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。
特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种直接调用IMP减少查找过程的方式可以提高程序的性能。NSObject类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。
当我们像一个对象发送消息[Receiver message],Receiver没有实现该消息,即[Receiver respondsToSelector:SEL]返回为NO情况下,其实系统不会立刻出现cash,这时Runtime system会对message进行转发。转发之后,如果该消息依然没有被执行就会出现Cash!Runtime System为我们提供了三种解决这种给对象发送没有实现消息方案。
消息转发机制基本上分为三个步骤:
1. 动态方法解析
2. 备用接收者
3. 完整转发
我们可以通过控制这三个步骤其中一环来解决这一个问题
特别注意:如果是正常类的消息,是不会走到这三个步骤的。所以走到这三个不步骤的前提条件已经确定该消息为未知消息
测试用例用到的源码
Boy.h
@interface Boy : NSObject
-(void)say:(NSString*)str girl:(NSString*)girl;
@end
Boy.m
@implementation Boy
-(void)say:(NSString*)str girl:(NSString*)girl
{
NSLog(@”%@”,str);
NSLog(@”%@”,girl);
}
@end
Girl.h
@interface Girl : NSObject
-(void)sayGirl:(NSString*)word;
@end
Girl.m
void dynamicMethodIMP(id self, SEL _cmd)
{
NSString *className = NSStringFromClass([self class]);
NSString *selName = NSStringFromSelector(_cmd);
NSLog(@”%@:不是%@的方法”,selName, className);//给用户警告但是不crash
}
@implementation Girl
-(void)sayGirl:(NSString*)word
{
NSLog(@”I am a girl”);
}
@end
动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法”“。不过使用该方法的前提是我们已经 实现了该”处理方法”,我们可以在运行时通过class_addMethod函数动态添加未知消息到类里面,让原来没有处理这个方法的类具有了处理这个方法的能力。
实例:
Girl *girl = [[Girl alloc] init];
[girl performSelector:@selector(sayGirl:) withObject:nil];
[girl performSelector:@selector(checkBoy) withObject:nil];
输出:
2015-09-22 15:14:58.619 Runtime[24078:1202259] I am a girl
2015-09-22 15:14:58.619 Runtime[24078:1202259] -[Girl checkBoy]: unrecognized selector sent to instance 0x100203cb0
可以看出系统cash一条unrecognized selector的异常。
现在我们在Girl.m实现+(BOOL)resolveInstanceMethod:(SEL)sel方法。类方法也类似。
+(BOOL)resolveInstanceMethod:(SEL)sel
{
//从系统里匹配SEL,如没有就注册SEL
SEL systemSel = sel_registerName(sel_getName(sel));
//把所有未知的SEL指向dynamicMethodIMP的实现,让dynamicMethodIMP帮忙打印错误信息,但是程序不会Cash
class_addMethod(self,systemSel,(IMP)dynamicMethodIMP,”v@:”);
return [super resolveClassMethod:systemSel];
}
再运行上面的实例代码时候就输出正常,并切不会cash
输出:
2015-09-22 15:17:25.639 Runtime[24223:1212399] I am a girl
2015-09-22 15:17:25.640 Runtime[24223:1212399] checkBoy:不是Girl的方法
对于dynamicMethodIMP里面处理的事情可以是提醒,也可以把这条消息转发给其他对象等。
备用接收者
如果在动态方法解析无法处理消息,则Runtime会继续调以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的对象,则返回的对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
在Boy.m里面添加如下代码
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return [[Girl alloc] init];
}
实例:
Boy *boy = [[Boy alloc] init];
[boy performSelector:@selector(sayGirl:) withObject:nil];//发一条girl的消息
[boy performSelector:@selector(checkBoy) withObject:nil];//发一条两个对象都无法识别的消息,由于girl可以接收任意消息所以这里也不会crash
输出:
2015-09-22 15:26:21.974 Runtime[24746:1250012] I am a girl //成功转发了消息
2015-09-22 15:26:21.974 Runtime[24746:1250012] checkBoy:不是Girl的方法
完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
此时会调用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
由于NSInvocation的初始化需要有一个方法签名NSMethodSignature,所以我们还需要实现下面的方法,该方法的返回值用于初始化NSInvocation的。
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
所以我们在Boy.m里面实现这两个函数,在实现这两个函数之前注释掉上一步添加的
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return [[Girl alloc] init];
}
因为如果在上一步中如果未知消息被处理就不会走到这里了,删除完之后在Boy.m里添加代码如下
//在这里产生方法签名,以确保NSInvocation能被转发的Girl类执行,不然的话会出现错误
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([Girl instancesRespondToSelector:aSelector]) {
signature = [Girl instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([Girl instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[[Girl alloc] init]];//将消息转发给Girl对象
}
}
实例:
[boy performSelector:@selector(sayGirl:) withObject:nil];
[boy performSelector:@selector(checkBoy) withObject:nil];
输出:
2015-09-23 09:31:50.388 Runtime[3583:151342] I am a girl
2015-09-23 09:31:50.388 Runtime[3583:151342] checkBoy:不是Girl的方法
输出结果与备用接收者那一步一样,表示我们转发成功
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。
消息转发与多重继承
回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这 种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能 集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转 发。
不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:
- (BOOL)respondsToSelector:(SEL)aSelector {
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can
*
* be forwarded to another object and whether that
*
* object can respond to it. Return YES if it can.
*/
}
return NO;
}
小结
在此,我们已经了解了Runtime中消息发送和转发的基本机制。这也是Runtime的强大之处,通过它,我们可以为程序增加很多动态的行为,虽 然我们在实际开发中很少直接使用这些机制(如直接调用objc_msgSend),但了解它们有助于我们更多地去了解底层的实现。其实在实际的编码过程中,我们也可以灵活地使用这些机制,去实现一些特殊的功能,如hook操作等。
参考:
http://www.cnblogs.com/ioshe/p/5489086.html
http://www.cocoachina.com/ios/20141107/10162.html
http://www.jianshu.com/p/41735c66dccb
OC学习Runtime之消息传递,消息转发机制 http://blog.youkuaiyun.com/u014410695/article/details/48650965