基于协议分发器的 A/B Test 方案
最近在看一些博客的时候,发现了一篇 iOS A/B Test 方案探索 ,突然想到在上家公司(做电商的)的时候,接到了产品提出的 A/B Test 需求的时候,非常的恐惧,当时并没有什么好方案,也是用最粗劣的方式实现的,看了这篇博客后,感觉当时能够想到或者看到这样的方案的话,就不会有这么多的苦恼了。
当时的苦恼
大多数情况下,A/B 方案基本上是 UI 全变,不同的用户看到的内容不尽相同,当时我们组的处理办法就是在各种代理的方法里面添加 if 或者是 switch 语句,这样导致代码结构很混乱,本来只有 300 多行代码的控制器,一下子变成了 600 行,后面改成用两个控制器做选择,但是这样修改的地方就很多了,而且在控制器外面判断也导致了诸多的问题,并非可行的方案,下面我们就说下 基于协议分发器的 A/B Test 方案 是怎么实现的吧。
可行发方案
- 抽离出 delegate 和 dataSource;即面对复杂的控制器,我们可以使用单独的类来实现这些代理方法。
- 协议分发;将协议与具体要实现的类结合起来,当需要此类去实现的时候,去指定具体的类去实现协议中的方法。
- 实现分发器的 “自释放” 。
具体的实现
抽离出 delegate 和 dataSource
@interface DelegateSource : NSObject <UITableViewDataSource, UITableViewDelegate>
@end
单独创建一个类,来实现复杂的 UI 中的一些视图和逻辑;
协议分发与 “自释放”
@interface ProtocolDispatcher ()
@property (nonatomic, strong) Protocol *prococol;
@property (nonatomic, strong) NSArray *implemertors;
@end
@implementation ProtocolDispatcher
+ (id)dispatcherProtocol:(Protocol *)protocol toImplemertors:(NSArray *)implemertors {
return [[ProtocolDispatcher alloc] initWithProtocol:protocol toImplemertors:implemertors];
}
- (instancetype)initWithProtocol:(Protocol *)protocol toImplemertors:(NSArray *)implemertors {
if (self = [super init]) {
self.prococol = protocol;
NSMutableArray *implemertorContexts = [NSMutableArray arrayWithCapacity:implemertors.count];
[implemertors enumerateObjectsUsingBlock:^(id implemertor, NSUInteger idx, BOOL * _Nonnull stop) {
ImplemertorContext *implemertorContext = [ImplemertorContext new];
implemertorContext.implemertor = implemertor;
[implemertorContexts addObject:implemertorContext];
// 由于分发器只是一个局部变量,将其放到给定的 implemertor 中,等 implemertor 释放是,分发器也会释放掉
objc_setAssociatedObject(implemertor, _cmd, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}];
self.implemertors = implemertorContexts;
}
return self;
}
这里将协议和类绑定在一起,待外部传入协议和类之后,分给实现了协议的类去处理,那么怎么知道一个类是否以及实现了协议里面的方法呢,系统以及有了 protocol_getMethodDescription
函数来查看协议中是否有对应的方法,如下
/**
如何做到只对Protocol中Selector函数的调用做分发是设计的关键,系统提供有函数
通过以下方法即可判断Selector是否属于某一Protocol
objc_method_description 的两个成员变量分别表示 运行时方法的名字和方法的参数
*/
struct objc_method_description MethodDescriptionForSELInProtocol(Protocol *protocol, SEL sel)
{
struct objc_method_description description = protocol_getMethodDescription(protocol, sel, YES, YES);
if (description.types)
{
return description;
}
description = protocol_getMethodDescription(protocol, sel, NO, YES);
if (description.types)
{
return description;
}
return (struct objc_method_description){NULL, NULL};
}
BOOL ProtocolContainSel(Protocol *protocol, SEL sel)
{
return MethodDescriptionForSELInProtocol(protocol, sel).types ? YES: NO;
}
另外,在 iOS 消息转发的动态特性中,我们可以实现一个类是否满足可以相应该方法。要注意重写 respondsToSelector
方法来判定是否可以相应此方法。
// 一:动态解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
// 二:快速转发
// 返回实现了方法的消息转发对象
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
// 三:慢速转发
// 函数签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
//函数调用
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
以上则是实现了 庞海礁 提到的协议分发器,此分发器可以将协议分发给多个实现者,如果函数有返回值,则前面返回的返回值会被后面返回的覆盖,即以数组最后一个可以实现该方法的类为准。但是我们需要实现的 A/B Test 是只需要有一个实现者即可,因此 李剑飞 在此基础上,修改了一下分发器的初始化方法
/**
协议分发器
@param protocol 遵循的协议;
@param indexImplemertor AB Test 需要执行的协议实现实例数组下标;
若传入 对应的 NSNumber 数字, 则调用改实现实例的协议方法;
若传入 nil,则调用全部的遵循协议的实现实例
@param implemertors 所有需要遵循协议的实现实例;
@return 协议分发器;
*/
+ (id)dispatcherProtocol:(Protocol *)protocol
withIndexImplemertor:(NSNumber *)indexImplemertor
toImplemertors:(NSArray *)implemertors;
通过传入的 number 来决定具体由哪个 implemertor
去实现此协议,具体可以参考他的 github
self.delegateSource_A = [UITableViewDelegateDataSource_A new];
self.delegateSource_B = [UITableViewDelegateDataSource_B new];
// A = 0
// B = 1
NSUInteger type = 1;
self.tableView.delegate = ABTestProtocolDispatcher(UITableViewDelegate,
@(type),
self.delegateSource_A,
self.delegateSource_B);
self.tableView.dataSource = ABTestProtocolDispatcher(UITableViewDataSource,
@(type),
self.delegateSource_A,
self.delegateSource_B);