动态的Objective-C——关于消息机制与运行时的探讨(二)

简介: 动态的Objective-C——关于消息机制与运行时的探讨

3.拯救未知消息的3根救命稻草


第一根救命稻草:


   如上所说,如果对象整个继承链都无法处理当前消息,那么首先会调用接收对象所属类的resolveInstanceMethod方法(这个对应实例方法,如果是无法处理的类方法消息,则会调用resolveClassMethod方法),在这个方法中,开发者有机会为类动态添加方法,如果动态添加了方法,可以在这个方法中返回YES,那么此条消息依然会被成功处理。例如我们将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类不做任何修改,当我们运行程序,程序会直接crash掉,现在我们在MyObject类中添加如下方法:


+(BOOL)resolveInstanceMethod:(SEL)sel{

   NSLog(@"resolveInstanceMethod");

   if ([NSStringFromSelector(sel) isEqualToString:@"showSelf"]) {

       class_addMethod(self, sel, newFunc, "v@:");

   }

   return [super resolveInstanceMethod:sel];

}

其中class_addMethod函数用来向类中动态添加方法,第一个参数为Class对象,第二个参数为方法选择器,第三个参数为IMP类型的函数指针,第四个参数为指定方法的返回值和参数类型。这个参数采用的是C字符串的形式来指定返回值和参数的类型,第1个字符为返回值类型,其后都为参数类型,需要注意,使用这种方式添加方法的时候系统会默认传入两个参数,分别是调用此方法的实例对象和方法选择器,上面示例代码中的"@"表示第1个id类型的参数,":"表示第2个选择器类型的参数,后面我会把字符所表示的参数类型映射表提供给大家。


   抽丝剥茧一下,IMP和SEL并不同,SEL可以理解为函数签名,其与函数名相关联,而IMP是函数所在地址的指针,其定义如下:


typedef void (*IMP)(void /* id, SEL, ... */ );

简单理解,通过IMP我们可以直接拿到函数的地址,后面会对函数做更深入的剖析,到时候你能就能豁然你开朗。  


   运行工程,根据打印信息可以看到showSelf方法被添加并正常执行了。


第二根救命稻草:


   抛开运行时添加方法这一手段,将resolveInstanceMethod方法删去,是不是我们的程序就必然走进crash的深渊了,其实不然,上帝还会给你另一根救命稻草,当通过运行时添加方法被否定后,系统会接着调用forwardingTargetForSelector方法,这个方法用来对消息进行转发,没错,重点来了,Objective-C中强大的消息转发机制的奥妙就在这里。forwardingTargetForSelector方法需要返回一个id类型的对象,系统会将当前对象服务处理的消息转发给这个方法返回的对象,如果这个返回的对象可以处理,那么程序依然可以很好的执行下去。


   例如,在我们的命令行工程中新添加一个类,命名为SubObject,实现如下:


SubObject.h文件:


#import <Foundation/Foundation.h>


@interface SubObject : NSObject


@end

SubObject.m文件:


#import "SubObject.h"


@implementation SubObject

-(void)showSelf{

   NSLog(@"subObject");

}

@end

在MyObject类中实现如下方法:


-(id)forwardingTargetForSelector:(SEL)aSelector{

   NSLog(@"forwardingTargetForSelector");

   if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {

       return [SubObject new];

   }

   return [super forwardingTargetForSelector:aSelector];

}

forwardingTargetForSelector方法可以返回一个对象,Objective-C会将当前对象无法处理的消息转发给这个方法返回的对象,如果返回nil,则表示不进行消息转发,这时你如果还想挽救此次crash,你就需要用到第三根救命稻草了。我们可以这种消息转发的机制来模拟Objective-C中的多继承。


第三根救命稻草:


   如果你不幸错过了前两次拯救未知消息的机会,那么你还有最后一次机会(中国有句古话,事不过三,世间万事也果真如此...)。当消息转发策略也被否定后,系统会调用methodSignatureForSelector方法,这个方法的主要用途是询问这个选择器是否是有效的,我们需要返回一个NSMethodSignature,顾名思义,这个对象是函数签名的抽象。如果我们返回了有效的函数签名,那么接着系统会调用forwardInvocation方法,这里是拯救应用程序的最后一根稻草了,这个函数会直接将消息包装成NSInvocation对象传入,我们直接将其发送给可以处理此消息的对象即可(当然你也可以直接抛弃,不理会这条未知的消息)。


   例如,在MyObject类中将forwardingTargetForSelector方法删去,实现如下两个方法:


//询问此选择器是否是有效的

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

   NSLog(@"methodSignatureForSelector");

   if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {

      return [[SubObject new] methodSignatureForSelector:aSelector];

   }

   return [super methodSignatureForSelector:aSelector];

}

//处理消息

-(void)forwardInvocation:(NSInvocation *)anInvocation{

   NSLog(@"forwardInvocation");

   if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"showSelf"]) {

       [anInvocation invokeWithTarget:[SubObject new]];

   }else{

       [super forwardInvocation:anInvocation];

   }

}

再次运行工程,程序又被你挽救了一次。


你真的需要救命稻草么?


   通过上面的三根救命稻草,我相信你一定对Objective-C消息机制有了全面而深入的了解,上面的代码也只是为了示例所用,正常情况下,你都不会使用到这些函数(毕竟如果你需要救命稻草,说明你已经落水了)。除非某些特殊需求或者做一些调试框架的开发,否则尽量不要介入消息的发送机制,就像生病就医,发现问题总比逃避治疗要好。顺便说一下,如果你没有使用任何救命稻草,当向某个对象发送了无法处理的消息时,系统会最终调用到NSObject类的doesNotRecognizeSelector方法,这个方法会抛出异常信息,正因如此,你在Xcode的控制台会经常看到如下图所示的crash信息:


image.png


你也可以重写这个方法来自定义输出信息,例如:


-(void)doesNotRecognizeSelector:(SEL)aSelector{

   NSLog(@"doesNotRecognizeSelector");

   if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {

       NSLog(@"not have a method named showSelf");

       return;

   }

   [super doesNotRecognizeSelector:aSelector];

}

下图完整展示了Objective-C整个消息发送与转发机制:


image.png



目录
相关文章
|
7月前
|
编译器 Swift iOS开发
44 Swift和Objective-C的运行时简介
Swift和Objective-C的运行时简介
45 0
|
iOS开发 开发者
动态的Objective-C——关于消息机制与运行时的探讨(四)
动态的Objective-C——关于消息机制与运行时的探讨
119 0
|
iOS开发
动态的Objective-C——关于消息机制与运行时的探讨(三)
动态的Objective-C——关于消息机制与运行时的探讨
195 0
动态的Objective-C——关于消息机制与运行时的探讨(三)
|
缓存 自然语言处理 IDE
动态的Objective-C——关于消息机制与运行时的探讨(一)
动态的Objective-C——关于消息机制与运行时的探讨
151 0
动态的Objective-C——关于消息机制与运行时的探讨(一)
|
测试技术 C语言 iOS开发
|
存储 C语言 iOS开发
(转载)Objective-C总Runtime的那点事儿(一)消息机制
原文地址:http://www.cocoachina.com/ios/20141018/9960.html 找工作,Objective-C中的Runtime是经常被问到的一个问题,几乎是面试大公司必问的一个问题。
969 0
|
iOS开发 编译器 C语言
objective-c下的消息机制
l   消息机制:   Objective-c下调用函数并不是采用的想C语言中的函数调用机制而是使用的一种消息传递的机制。下文将讨论这两种协议之间的区别。   传统的函数调用机制是在程序编译的阶段就已经将子函数的引用地址编译进了执行代码,所以当编译完成之后,函数名指向的就是函数的入口地址,而调用函数将直接导向至函数的入口地址,从而直接开始执行函数。   而Objective-c采用的
963 0
|
Android开发 iOS开发
利用Objective-C运行时hook函数的三种方法
方法一,hook已有公开头文件的类: 首先写一个Utility函数: #import void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) { Method oldMethod ...
1199 0