基于协议分发器的 A/B Test 方案

2017-10-26  本文已影响110人  AppleTTT

最近在看一些博客的时候,发现了一篇 iOS A/B Test 方案探索 ,突然想到在上家公司(做电商的)的时候,接到了产品提出的 A/B Test 需求的时候,非常的恐惧,当时并没有什么好方案,也是用最粗劣的方式实现的,看了这篇博客后,感觉当时能够想到或者看到这样的方案的话,就不会有这么多的苦恼了。

当时的苦恼

大多数情况下,A/B 方案基本上是 UI 全变,不同的用户看到的内容不尽相同,当时我们组的处理办法就是在各种代理的方法里面添加 if 或者是 switch 语句,这样导致代码结构很混乱,本来只有 300 多行代码的控制器,一下子变成了 600 行,后面改成用两个控制器做选择,但是这样修改的地方就很多了,而且在控制器外面判断也导致了诸多的问题,并非可行的方案,下面我们就说下 基于协议分发器的 A/B Test 方案 是怎么实现的吧。

可行发方案

  1. 抽离出 delegate 和 dataSource;即面对复杂的控制器,我们可以使用单独的类来实现这些代理方法。
  2. 协议分发;将协议与具体要实现的类结合起来,当需要此类去实现的时候,去指定具体的类去实现协议中的方法。
  3. 实现分发器的 “自释放” 。

具体的实现

抽离出 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);

参考资料

  1. iOS A/B Test 方案探索
  2. Protocol 协议分发器
  3. iOS 释放自注销模式设计
上一篇下一篇

猜你喜欢

热点阅读