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的文章实现的,但是做了一些额外的优化
,优化点在于但不局限
- 传递参数的封包和解包,主要处理的是非id类型的转化
- 对于未实现的方法有相关的警告⚠️,从而避免事件传递丢弃
- 支持传递过程中的拦截和修改参数再传递行为
// 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 的职责主要有两点:
- 建立Map负责创建和存储,方法Key和NSInvocation实例
- 提供入口函数处理或拦截的方法Key并执行NSInvocation相关内容
- 如下是它的主要核心函数
/**
* 为当前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 分类实现
- 为最终的响应UIResponder也就是UIViewController实现分类挂上一个EventProxy 属性,负责存储其需要处理的响应
- 为需要拦截响应UIResponder也就是中间UIView实现分类挂上一个EventProxy 属性,负责存储响应,可以做的是处理响应、拦截并终止传递响应和拦截并修改参数后继续传递响应等行为
- 在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的。
举例子:重点解释这里
- kAction_button3_action 是指方法KEY与在该响应链的方法签名是一一对应的,可以这么说。
- msgSource:self 是优化的另一个点,实际是上是传递当前起点对象,方便响应者做额外的分析处理
- 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转发消息
核心函数
- 通过方法和方法参数数组,我们可以做的是匹配入参无误之后,获取参数签名类型把参数一一解包并调用invocation的setArgument:atIndex:设置参数
- 如下分别给出了匹配入口的校验以及解包的关键部分代码
- 最后考虑是否有返回值,这里虽然补充有但不建议考虑在这给返回值,意义不大。
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:¶mater 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或代理层层转发出来了,效果就是这样子。
总结
- 对于层级在两级或以上的控制器这种方式在处理事件和参数传递上
异常的简单和快捷
- 目前项目也一直在使用这样方式来处理这样的事件和参数传递,没有丝毫拖拉项目开发反而节省了不少额外代码和开发时间。
- 需要注意的是代码的规范和开发人员的代码风格意识培养,执行并大体上认可。
- 目前该框架做为一个基本组件在稳定实践。