本文Demo传送门: MessageForwardingDemohtml
摘要:编程,只了解原理不行,必须实战才能知道应用场景。本系列尝试阐述runtime相关理论的同时介绍一些实战场景,而本文则是本系列的消息转发篇。本文中,第一节将介绍方法消息发送相关的概念,第二节将总结一下2. 动态特性:方法解析和消息转发(Method Resolution,Fast Rorwarding,Normal Forwarding),第三节将介绍方法交换几种的实战场景:特定奔溃预防处理(调用未实现方法),苹果系统迭代形成API不兼容的奔溃处理,第四节将总结消息转发的机制。git
在咱们开始使用消息机制以前,咱们能够约定咱们的术语。例如,不少人不清楚“方法”与“消息”是什么,但这对于理解消息传递系统如何在低级别工做相当重要。github
- (int)meaning { return 42; }
meaning
而且没有参数。@selector(meaning)
。消息在OC中方法调用是一个消息发送的过程。OC方法最终被生成为C函数,并带有一些额外的参数。这个C函数objc_msgSend
就负责消息发送。在runtime的objc/message.h
中能找到它的API。面试
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)`
复制代码
消息发送的时候,在C语言函数中发生了什么事情?编译器是如何找到这个方法的呢?消息发送的主要步骤以下:编程
其中,为何它被称为 “转发”? 当某个对象没有任何响应某个 消息 的操做就 “转发” 该 消息。缘由是这种技术主要是为了让对象让其余对象为他们处理 消息,从而 “转发”。bash
消息转发是一种功能强大的技术,能够大大增长Objective-C的表现力。什么是消息转发?简而言之,它容许未知的消息被困住并做出反应。换句话说,不管什么时候发送未知消息,它都会以一个很好的包发送到您的代码中,此时您能够为所欲为地执行任何操做。app
OC中的方法默认被隐藏了两个参数:self
和_cmd
。你可能知道self
是做为一个隐式参数传递的,它最终成为一个明确的参数。不为人知的隐式参数_cmd
(它保存了正在发送的消息的选择器)是第二个这样的隐式参数。总之,self
指向对象自己,_cmd
指向方法自己。举两个例子来讲明:ide
例1:- (NSString *)name
这个方法实际上有两个参数:self
和_cmd
。函数
例2:- (void)setValue:(int)val
这个方法实际上有三个参数:self
,_cmd
和 val
。性能
在编译时你写的 OC 函数调用的语法都会被翻译成一个 C 的函数调用 objc_msgSend()
。好比,下面两行代码就是等价的:
[array insertObject:foo atIndex:5];
复制代码
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
复制代码
其中的objc_msgSend
就负责消息发送。
没有方法的实现,程序会在运行时挂掉并抛出 unrecognized selector sent to …
的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:
首先,Objective-C 运行时会调用 + (BOOL)resolveInstanceMethod:
或者 + (BOOL)resolveClassMethod:
,让你有机会提供一个函数实现。若是你添加了函数并返回 YES, 那运行时系统就会从新启动一次消息发送的过程。仍是以 foo 为例,你能够这么实现:
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}
复制代码
这里第一字符v
表明函数返回类型void
,第二个字符@
表明self的类型id
,第三个字符:
表明_cmd的类型SEL
。这些符号可在Xcode中的开发者文档中搜索Type Encodings就可看到符号对应的含义,更详细的官方文档传送门 在这里,此处再也不列举了。
与下面2.3完整转发不一样,Fast Rorwarding这是一种快速消息转发:只须要在指定API方法里面返回一个新对象便可,固然其它的逻辑判断仍是要的(好比该SEL是否某个指定SEL?)。
消息转发机制执行前,runtime系统容许咱们替换消息的接收者为其余对象。经过- (id)forwardingTargetForSelector:(SEL)aSelector
方法。若是此方法返回的是nil 或者self,则会进入消息转发机制(- (void)forwardInvocation:(NSInvocation *)invocation
),不然将会向返回的对象从新发送消息。
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(foo:)){
return [[BackupClass alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
复制代码
与上面不一样,能够理解成完整消息转发,是能够代替快速转发作更多的事。
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = invocation.selector;
if([alternateObject respondsToSelector:sel]) {
[invocation invokeWithTarget:alternateObject];
} else {
[self doesNotRecognizeSelector:sel];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}
复制代码
forwardInvocation:
方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不一样的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,所以没有响应也不会报错。例如:咱们能够为了不直接闪退,能够当消息无法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。
其中,参数invocation
是从哪来的?在forwardInvocation:
消息发送前,runtime系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation对象。因此重写forwardInvocation:
的同时也要重写methodSignatureForSelector:
方法,不然会抛出异常。当一个对象因为没有相应的方法实现而没法响应某个消息时,运行时系统将经过forwardInvocation:
消息通知该对象。每一个对象都继承了forwardInvocation:
方法,咱们能够将消息转发给其它的对象。
可能有朋友看到,这两个转发都是将消息转发给其它对象,那么这两个有什么区别?
须要重载的API方法的用法不一样
invokeWithTarget:
)。转发给新对象的个数不一样
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector==@selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector =[anInvocation selector];
RunPerson *RP1=[RunPerson new];
RunPerson *RP2=[RunPerson new];
if ([RP1 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:RP1];
}
if ([RP2 respondsToSelector:selector]) {
[anInvocation invokeWithTarget:RP2];
}
}
复制代码
下面有一段由于没有实现方法而会致使奔溃的代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor whiteColor]];
self.title = @"Test2ViewController";
//实例化一个button,未实现其方法
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(50, 100, 200, 100);
button.backgroundColor = [UIColor blueColor];
[button setTitle:@"消息转发" forState:UIControlStateNormal];
[button addTarget:self
action:@selector(doSomething)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
复制代码
为解决这个问题,能够专门建立一个处理这种问题的分类:
#import "NSObject+CrashLogHandle.h"
@implementation NSObject (CrashLogHandle)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
//方法签名
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}
@end
复制代码
由于在category中复写了父类的方法,会出现下面的警告:
解决办法就是在Xcode的Build Phases中的资源文件里,在对应的文件后面 -w ,忽略全部警告。
随着每一年iOS系统与硬件的更新迭代,部分性能更优异或者可读性更高的API将有可能对原有API进行废弃与更替。与此同时咱们也须要对现有APP中的老旧API进行版本兼容,固然进行版本兼容的方法也有不少种,下面笔者会列举经常使用的几种:
if ([object respondsToSelector: @selector(selectorName)]) {
//using new API
} else {
//using deprecated API
}
复制代码
if (NSClassFromString(@"ClassName")) {
//using new API
}else {
//using deprecated API
}
复制代码
#define isOperatingSystemAtLeastVersion(majorVersion, minorVersion, patchVersion)[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: (NSOperatingSystemVersion) {
majorVersion,
minorVersion,
patchVersion
}]
if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
//using new API
} else {
//using deprecated API
}
复制代码
需求:假设如今有一个利用新API写好的类,以下所示,其中有一行可能由于运行在低版本系统(好比iOS9)致使奔溃的代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor whiteColor]];
self.title = @"Test3ViewController";
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 375, 600) style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
tableView.backgroundColor = [UIColor orangeColor];
// May Crash Line
tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
[self.view addSubview:tableView];
}
复制代码
其中有一行会发出警告,Xcode也给出了推荐解决方案,若是你点击Fix它会自动添加检查系统版本的代码,以下图所示:
方案1:手动加入版本判断逻辑
之前的适配处理,可根据操做系统版本进行判断
if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
viewController.automaticallyAdjustsScrollViewInsets = NO;
}
复制代码
方案2:消息转发
在iOS11 Base SDK直接采起最新的API而且配合Runtime的消息转发机制就能实现一行代码在不一样版本操做系统下采起不一样的消息调用方式
#import "UIScrollView+Forwarding.h"
#import "NSObject+AdapterViewController.h"
@implementation UIScrollView (Forwarding)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1
NSMethodSignature *signature = nil;
if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) {
signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
}else {
signature = [super methodSignatureForSelector:aSelector];
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation { // 2
BOOL automaticallyAdjustsScrollViewInsets = NO;
UIViewController *topmostViewController = [self cm_topmostViewController];
NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3
[viewControllerInvocation setTarget:topmostViewController];
[viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
[viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4
[viewControllerInvocation invokeWithTarget:topmostViewController]; // 5
}
@end
复制代码
#import "NSObject+AdapterViewController.h"
@implementation NSObject (AdapterViewController)
- (UIViewController *)cm_topmostViewController {
UIViewController *resultVC;
resultVC = [self cm_topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
while (resultVC.presentedViewController) {
resultVC = [self cm_topViewController:resultVC.presentedViewController];
}
return resultVC;
}
- (UIViewController *)cm_topViewController:(UIViewController *)vc {
if ([vc isKindOfClass:[UINavigationController class]]) {
return [self cm_topViewController:[(UINavigationController *)vc topViewController]];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
return [self cm_topViewController:[(UITabBarController *)vc selectedViewController]];
} else {
return vc;
}
}
@end
复制代码
当咱们在iOS10调用新API时,因为没有具体对应API实现,咱们将其原有的消息转发至当前栈顶UIViewController去调用低版本API。
关于[self cm_topmostViewController];
,执行以后获得的结果能够查看以下:
方案2的总体流程:
为即将转发的消息返回一个对应的方法签名(该签名后面用于对转发消息对象(NSInvocation *)anInvocation进行编码用)
开始消息转发((NSInvocation *)anInvocation封装了原有消息的调用,包括了方法名,方法参数等)
因为转发调用的API与原始调用的API不一样,这里咱们新建一个用于消息调用的NSInvocation对象viewControllerInvocation并配置好对应的target与selector
配置所需参数:因为每一个方法实际是默认自带两个参数的:self和_cmd,因此咱们要配置其余参数时是从第三个参数开始配置
消息转发
注意测试的时候,选择iOS10系统的模拟器进行验证(没有的话能够先Download Simulators),安装完后以下如选择:
会以下图所示奔溃:
面试挖坑:OC是否支持多继承?好,你说不支持多继承,那你有没有模拟多继承特性的办法?
转发和继承类似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另外一个对象中放法接过来或者“继承”同样。消息转发弥补了objc不支持多继承的性质,也避免了由于多继承致使单个类变得臃肿复杂。
虽然转发能够实现继承功能,可是NSObject仍是必须表面上很严谨,像respondsToSelector:
和isKindOfClass:
这类方法只会考虑继承体系,不会考虑转发链。
Objective-C 中给一个对象发送消息会通过如下几个步骤:
在对象类的 dispatch table 中尝试找到该消息。若是找到了,跳到相应的函数IMP去执行实现代码;
若是没有找到,Runtime 会发送 +resolveInstanceMethod:
或者 +resolveClassMethod:
尝试去 resolve 这个消息;
若是 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector:
容许你把这个消息转发给另外一个对象;
若是没有新的目标对象返回, Runtime 就会发送-methodSignatureForSelector:
和 -forwardInvocation:
消息。你能够发送 -invokeWithTarget:
消息来手动转发消息或者发送 -doesNotRecognizeSelector:
抛出异常。