动态的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



目录
相关文章
|
编译器 Swift iOS开发
44 Swift和Objective-C的运行时简介
Swift和Objective-C的运行时简介
101 0
|
iOS开发
动态的Objective-C——关于消息机制与运行时的探讨(三)
动态的Objective-C——关于消息机制与运行时的探讨
221 0
动态的Objective-C——关于消息机制与运行时的探讨(三)
|
缓存 自然语言处理 IDE
动态的Objective-C——关于消息机制与运行时的探讨(一)
动态的Objective-C——关于消息机制与运行时的探讨
186 0
动态的Objective-C——关于消息机制与运行时的探讨(一)
|
程序员 编译器 C#
Objective-C中runtime机制的应用(二)
Objective-C中runtime机制的应用
131 0
Objective-C中runtime机制的应用(二)
|
安全 编译器 iOS开发
Objective-C中runtime机制的应用(一)
Objective-C中runtime机制的应用
128 0
Objective-C中runtime机制的应用(一)
|
iOS开发 开发者
动态的Objective-C——关于消息机制与运行时的探讨(四)
动态的Objective-C——关于消息机制与运行时的探讨
146 0
|
存储 C语言 iOS开发
(转载)Objective-C总Runtime的那点事儿(一)消息机制
原文地址:http://www.cocoachina.com/ios/20141018/9960.html 找工作,Objective-C中的Runtime是经常被问到的一个问题,几乎是面试大公司必问的一个问题。
1002 0
|
6月前
|
安全 编译器 Swift
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
392 2
|
4月前
|
开发工具 iOS开发 容器
【Azure Blob】关闭Blob 匿名访问,iOS Objective-C SDK连接Storage Account报错
iOS Objective-C 应用连接Azure Storage时,若不关闭账号的匿名访问,程序能正常运行。但关闭匿名访问后,上传到容器时会出现错误:“Public access is not permitted”。解决方法是将创建容器时的公共访问类型从`AZSContainerPublicAccessTypeContainer`改为`AZSContainerPublicAccessTypeOff`,以确保通过授权请求访问。
【Azure Blob】关闭Blob 匿名访问,iOS Objective-C SDK连接Storage Account报错
|
6月前
|
缓存 开发工具 iOS开发
优化iOS中Objective-C代码调起支付流程的速度
优化iOS中Objective-C代码调起支付流程的速度
104 2