iOS之基于ResponderChain消息转发

什么是基于ResponderChain消息转发

本文主要思路来自于Casa这篇文章
链接: 一种基于ResponderChain的对象交互方式.
在此非常感谢Casa大神的分享,本人经常看他写的博客,他的每一篇博客都可以让我细细学习和实践。

前言
传统iOS的对象间交互模式就那么几种:直接property传值、delegate、KVO、block、protocol、多态、Target-Action。现在介绍的是一种全新的交互传值方式以及这种方式的应用场景: 基于ResponderChain来实现对象间交互

简单介绍
这种方式通过在UIResponder上挂一个category,使得事件和参数可以沿着responder chain逐步传递。

优点缺点
这相当于借用responder chain实现了一个自己的事件传递链。 这在事件需要层层传递的时候特别好用,然而这种对象交互方式的有效场景 仅限于在responder chain上的UIResponder对象上。它可以无视命名域的存在。如果采用传统的delegate层层传递的方式,由于delegate需要protocol的声明,因此就无法做到命名域隔离。但如果走Responder Chain,即使是另一个UI组件产生了事件,这个事件就可以被传递到其他组件的UI上。

实现原理

正如开篇介绍那样,给UIResponder开一个category,为你的这一事件定义一个方法或Key,把想要传递的值放上,借着事件响应链的传递,传递到你需要处理的UIResponder上。最终是通过重写performSelector:object:拿出方法签名赋参再[invocation invoke]。

这里的封装是基于casa的文章实现的,但是做了一些额外的优化,优化点在于但不局限

  1. 传递参数的封包和解包,主要处理的是非id类型的转化
  2. 对于未实现的方法有相关的警告⚠️,从而避免事件传递丢弃
  3. 支持传递过程中的拦截和修改参数再传递行为
// UIResponder 分类实现
/**
 *  (Casa的思想实践)
 *  KEY与方法一一对应转响应
 *  最终在控制器里要实现的是【routerEventWithSelectorKey:msgSource:objects】由此方法分发事件统一管理
*/
- (void)routerEventWithSelectorKey:(NSString *)selectorKey msgSource:(id)msgSource object:(id)object;
- (void)routerEventWithSelectorKey:(NSString *)selectorKey msgSource:(id)msgSource objects:(NSArray *)objects;
- (void)routerEventWithSelectorKey:(NSString *)selectorKey msgSource:(id)msgSource object:(id)object{
    NSArray *array = object ? @[object] : @[];
    [self routerEventWithSelectorKey:selectorKey msgSource:msgSource objects:array];
}

- (void)routerEventWithSelectorKey:(NSString *)selectorKey msgSource:(id)msgSource objects:(NSArray *)objects{
    [self.nextResponder routerEventWithSelectorKey:selectorKey msgSource:msgSource objects:objects];
}
// EventProxy 实现

EventProxy 的职责主要有两点:

  1. 建立Map负责创建和存储,方法Key和NSInvocation实例
  2. 提供入口函数处理或拦截的方法Key并执行NSInvocation相关内容
  3. 如下是它的主要核心函数
/**
 *  为当前EventProxy 所有响应的入口
 */
- (void)handleForKey:(NSString *)key msgSource:(id)msgSource objects:(NSArray *)objects{
    NSInvocation *invocation = [self.eventStrategy objectForKey:key];
    if (!invocation) {
        NSAssert(NO, @"not handleForKey");
    }
    
    // 0.初始化参数容器
    objects = objects?:@[];
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:(1+objects.count)];
    
    // 1.处理msgSource, 确保第一位参数是targetSource
    [array addObject:(msgSource?:[NSNull null])];
    
    // 2.处理业务参数
    [array addObjectsFromArray:objects];
    
    // 3.转发消息[注意target是谁,谁就负责响应事件]
    [invocation.target mmEvent_performSelector:invocation.selector objects:array];
}
// UIViewController 分类实现
// UIView 分类实现
  1. 为最终的响应UIResponder也就是UIViewController实现分类挂上一个EventProxy 属性,负责存储其需要处理的响应
  2. 为需要拦截响应UIResponder也就是中间UIView实现分类挂上一个EventProxy 属性,负责存储响应,可以做的是处理响应、拦截并终止传递响应和拦截并修改参数后继续传递响应等行为
  3. UIViewController分类里直接执行handleForKey:msgSource:objects:函数,因为在业务上,我们需要传递的参数顶端也只会到UIViewController,其都不处理,那传递也就毫无意义了。

UIResponder的分类.h

@interface UIViewController (MMEvent)
/**
 *  最终汇总
 */
@property (nonatomic, strong) MMEventProxy *eventProxy;

@end

@interface UIView (MMEvent)
/**
 *  subView拦截器
 */
@property (nonatomic, strong) MMEventProxy *eventProxy;

@end

UIResponder的分类.m

@implementation UIViewController (MMEvent)

- (void)setEventProxy:(MMEventProxy *)eventProxy{
    objc_setAssociatedObject(self, @selector(eventProxy), eventProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (MMEventProxy *)eventProxy{
    MMEventProxy *proxy = (MMEventProxy *)objc_getAssociatedObject(self, @selector(eventProxy));
    if (!proxy) {
        proxy = [[MMEventProxy alloc] init];
        [self setEventProxy:proxy];
    }
    return proxy;
}

/**
 *  最终响应可能会到达每个控制器, 所以直接响应
 */
- (void)routerEventWithSelectorKey:(NSString *)selectorKey msgSource:(id)msgSource objects:(NSArray *)objects{
    [self.eventProxy handleForKey:selectorKey msgSource:msgSource objects:objects];
}

@end

@implementation UIView (MMEvent)

- (void)setEventProxy:(MMEventProxy *)eventProxy{
    objc_setAssociatedObject(self, @selector(eventProxy), eventProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (MMEventProxy *)eventProxy{
    MMEventProxy *proxy = (MMEventProxy *)objc_getAssociatedObject(self, @selector(eventProxy));
    if (!proxy) {
        proxy = [[MMEventProxy alloc] init];
        [self setEventProxy:proxy];
    }
    return proxy;
}

@end

重点在UIViewController分类的routerEventWithSelectorKey:msgSource:objects:的处理,也就是EventProxy的核心函数

非对象类型参数的封包和解包

如下是一个subview传递响应给controller的参数,如何被塞进invocation里invoke的。

举例子:重点解释这里

  1. kAction_button3_action 是指方法KEY与在该响应链的方法签名是一一对应的,可以这么说。
  2. msgSource:self 是优化的另一个点,实际是上是传递当前起点对象,方便响应者做额外的分析处理
  3. objects: 是参数的数组,数组的大小和方法参数签名是一一对应的传递,当为nil也需要传递一个NSNull.new占位,这里传递对象类不需要封包,传递非对象类封包,封包是指给定一个数据格式,再根据方法签名位获取到参数类型时,再转化出来。我这里就同一使用@(xxx)来封包非对象类。
- (void)buttonClick3:(UIButton *)button{
    [self routerEventWithSelectorKey:kAction_button3_action msgSource:self objects:@[button, @(button.tag), @(button.frame), NSNull.new]];
}

解包并通过runtime转发消息
核心函数

  1. 通过方法和方法参数数组,我们可以做的是匹配入参无误之后,获取参数签名类型把参数一一解包并调用invocation的setArgument:atIndex:设置参数
  2. 如下分别给出了匹配入口的校验以及解包的关键部分代码
  3. 最后考虑是否有返回值,这里虽然补充有但不建议考虑在这给返回值,意义不大。
	NSMethodSignature *signature = [self.class instanceMethodSignatureForSelector:selector];
    if (!signature) {
        NSString *info = [NSString stringWithFormat:@"-[%@ %@]:unrecognized selector sent to instance",[self class],NSStringFromSelector(selector)];
        @throw [[NSException alloc] initWithName:@"unrecognized selector" reason:info userInfo:nil];
        return nil;
    }
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    if (invocation.methodSignature.numberOfArguments - 2 != objects.count) {
        NSAssert(NO, @"方法签名异常[入参数异常]");
        return nil;
    }
 NSMethodSignature *sig = [self methodSignatureForSelector:selector];
    // numberOfArguments存在默认的 _cmd、target 两个参数,需剔除
    for (NSInteger idx = 0; idx < objects.count; idx++) {
        id paramater = [objects objectAtIndex:idx];
        NSUInteger argIndex = idx + 2;
        
        char *argumentType = (char *)[sig getArgumentTypeAtIndex:argIndex];
        if ([paramater isKindOfClass:[NSNumber class]]) {
            NSNumber *paramaterNumberObj = (NSNumber *)paramater;
            if (!strcmp(argumentType, @encode(char))) {
                char value = paramaterNumberObj.charValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(unsigned char))) {
                unsigned char value = paramaterNumberObj.unsignedCharValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(short))) {
                short value = paramaterNumberObj.shortValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(unsigned short))) {
                unsigned short value = paramaterNumberObj.unsignedShortValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(int))) {
                int value = paramaterNumberObj.intValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(unsigned int))) {
                unsigned int value = paramaterNumberObj.unsignedIntValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(long))) {
                long value = paramaterNumberObj.longValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(unsigned long))) {
                unsigned long value = paramaterNumberObj.unsignedLongValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(long long))) {
                long long value = paramaterNumberObj.longLongValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(unsigned long long))) {
                unsigned long long value = paramaterNumberObj.unsignedLongLongValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(float))) {
                float value = paramaterNumberObj.floatValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(double))) {
                double value = paramaterNumberObj.doubleValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(BOOL))) {
                BOOL value = paramaterNumberObj.boolValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(NSInteger))) {
                NSInteger value = paramaterNumberObj.integerValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (!strcmp(argumentType, @encode(NSUInteger))) {
                NSUInteger value = paramaterNumberObj.unsignedIntegerValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else{
                NSAssert(NO, @"遇见了未知类型,急需适配并转发");
            }
            continue;
        } else if ([paramater isKindOfClass:[NSValue class]]) {
            NSValue *paramaterValueObj = (NSValue *)paramater;
            if (strcmp(argumentType, @encode(CGPoint)) == 0) {
                CGPoint value = paramaterValueObj.CGPointValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(CGSize)) == 0) {
                CGSize value = paramaterValueObj.CGSizeValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(CGRect)) == 0) {
                CGRect value = paramaterValueObj.CGRectValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(CGVector)) == 0) {
                CGVector value = paramaterValueObj.CGVectorValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(CGAffineTransform)) == 0) {
                CGAffineTransform value = paramaterValueObj.CGAffineTransformValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(CATransform3D)) == 0) {
                CATransform3D value = paramaterValueObj.CATransform3DValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(NSRange)) == 0) {
                NSRange value = paramaterValueObj.rangeValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(UIOffset)) == 0) {
                UIOffset value = paramaterValueObj.UIOffsetValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else if (strcmp(argumentType, @encode(UIEdgeInsets)) == 0) {
                UIEdgeInsets value = paramaterValueObj.UIEdgeInsetsValue;
                [invocation setArgument:&value atIndex:argIndex];
            } else {
                NSAssert(NO, @"遇见了未知类型,急需适配并转发");
            }
            continue;
        } else if ([paramater isKindOfClass:[NSNull class]]) {
            id value = nil;
            [invocation setArgument:&value atIndex:argIndex];
            continue;
        }
    
        [invocation setArgument:&paramater atIndex:argIndex];
    }
    [invocation invoke];

应用场景

应用场景比较单一,因为依赖UIResponder。所以可以想象到可以这么用,它可以完全取代由子view传递
假设当前的业务是:
控制器上有一个列表table,列表cell上有一个横向滑动的子列表collection,我们需要把横向滑动的子子列表collectionCell的事件和参数传递到当前的控制器
我们一般采取的方案是:block,delegate,用notification就过分了。
层级如下:

		collectionCell		-事件AKey,事件B key
	tablecell 				- EventProxy:注册并创建针对于事件B key的拦截 处理事件B key改参,转成事件Ckey
controller 					- EventProxy:注册并创建针对于事件AKey,事件Ckey,最终响应的是 事件AKey,事件Ckey

这样子就不需要通过block或代理层层转发出来了,效果就是这样子。

总结

  1. 对于层级在两级或以上的控制器这种方式在处理事件和参数传递上异常的简单和快捷
  2. 目前项目也一直在使用这样方式来处理这样的事件和参数传递,没有丝毫拖拉项目开发反而节省了不少额外代码和开发时间。
  3. 需要注意的是代码的规范和开发人员的代码风格意识培养,执行并大体上认可。
  4. 目前该框架做为一个基本组件在稳定实践。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值