Runtime——消息转发与运用

本文主要是针对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 contentInsetAdjustmentBehaviorautomaticallyAdjustsScrollViewInsets做栗子

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地址。

你可能感兴趣的:(Runtime——消息转发与运用)