本文主要是针对runtime消息转发进行整理,并举例关于消息转发的运用。
消息转发
1、消息调用
OC中发送消息是通过objc_msgSend(id, SEL, ...) 来实现的,首先会根据isa所指向的类结构中进行方法查找(objc_method_list),如果该类中无法查找到所对应的方法,则会沿类结构中的超类指针super_class继续向上索引,直至NSObject类。一旦索引到对应方法则会向该方法传递receiver对应的数据结构,同时,为了优化索引速度,系统会缓存每次成功索引的方法名和实现地址到类结构中的objc_cache。后续的方法索引将优先索引缓存中的方法列表。
通过isa查找,流程如下:
if (没有找到cache、objc_method_list,向父类索引至NSObject类) {
则去实现了动态方法方法解析。
}
else if ( 如果没有实现动态方法解析或者解析失败并且实现了消息转发机制) {
进入消息转发流程
}
else 程序crash
2、动态方法解析
如果调用的是实例方法则会调起该方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
如果调用的是类方法则会调起该方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
举个:
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
3、消息转发
未能向调用方法提供具体实现时即+ (BOOL)resolveInstanceMethod:(SEL)sel;或+ (BOOL)resolveClassMethod:(SEL)sel;返回值为NO。此时将转入消息转发流程。
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
这个方法将是消息转发的最后机会,我们可以利用它将原有的消息转发至另外的对象或者忽略。其中参数anInvocation是基于面向对象对原有方法调用的一层封装,包含了方法名、调用参数、方法签名等。
举个:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];//避免未处理而导致的Crash
}
运用
随着iOS系统版本的更新,部分性能更优异或者可读性更高的API将有可能对原有API进行废弃与更替。因此在开发中经常需要考虑、判断版本,那是不是可以考虑用runtime来进行动态处理?
下面主要是针对适配iOS 11 contentInsetAdjustmentBehavior与automaticallyAdjustsScrollViewInsets做栗子
1、新建一个Category(RTForwarding)
用于调用到iOS 11属性contentInsetAdjustmentBehavior的封装处理
代码如下:
//重写runtime方法
//1.为即将转发的消息返回一个对应的方法签名(该签名后面用于对转发消息对象(NSInvocation *)anInvocation进行编码用)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1
NSMethodSignature *signature = nil;
if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) {
signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
}else {
signature = [super methodSignatureForSelector:aSelector];
}
return signature;
}
//2.开始消息转发((NSInvocation *)anInvocation封装了原有消息的调用,包括了方法名,方法参数等)
- (void)forwardInvocation:(NSInvocation *)anInvocation { // 2
BOOL automaticallyAdjustsScrollViewInsets = NO;
UIViewController *topmostViewController = [self getTopmostViewController];
//3.由于转发调用的API与原始调用的API不同,这里我们新建一个用于消息调用的NSInvocation对象viewControllerInvocatio并配置好对应的target与selector
NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3
[viewControllerInvocation setTarget:topmostViewController];
[viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
//4.配置所需参数:由于每个方法实际是默认自带两个参数的:self和_cmd,所以我们要配置其他参数时是从第三个参数开始配置
[viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4
//5.消息转发
[viewControllerInvocation invokeWithTarget:topmostViewController]; // 5
}
//获取栈顶控制器
- (UIViewController *)getTopmostViewController {
UIViewController *resultVC;
resultVC = [self getTopmostViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
while (resultVC.presentedViewController) {
resultVC = [self getTopmostViewController:resultVC.presentedViewController];
}
return resultVC;
}
- (UIViewController *)getTopmostViewController:(UIViewController *)vc {
if ([vc isKindOfClass:[UINavigationController class]]) {
return [self getTopmostViewController:[(UINavigationController *)vc topViewController]];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
return [self getTopmostViewController:[(UITabBarController *)vc selectedViewController]];
} else {
return vc;
}
}
2、调用Category(RTForwarding)
在需要使用的地方导入头文件
#import "UIScrollView+RTForwarding.h"
在使用到iOS 11属性contentInsetAdjustmentBehavior时,不需要进行判断就可以实现之前需要判断功能。
代码如下:
CGSize main = [UIScreen mainScreen].bounds.size;
UITableView * tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, main.width, main.height - 64)];
tableView.delegate = self;
tableView.dataSource = self;
tableView.backgroundColor = [UIColor orangeColor];
tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;//无需判断,简单粗暴
[self.view addSubview:tableView];
理解了消息转发,在应用上还是能够出其不意达到简单粗暴的效果。本文主要是参考来源与链接,该文作者写的贼棒,本文主要是对自己的理解进行归纳整理,让自己的思路更清晰,当然如果有写的不对的地方欢迎指出。最后,当然也是最重要的附上demo地址。