美文网首页
Runtime——消息转发与运用

Runtime——消息转发与运用

作者: 走着走着就会敲代码了 | 来源:发表于2017-12-07 16:06 被阅读37次

    本文主要是针对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——消息转发与运用

          本文链接:https://www.haomeiwen.com/subject/bavdixtx.html