Mixpanel 可视化ABTest分析 —— iOS篇

因为这篇文章主要分析Mixpanel中可视化ABTest部分,所以代码中会忽略其他部分代码,其他代码部分会用"…"表示,且文章描述中仅针对ABTest部分。

1. 添加手势开启websocket

Mixpanel的初始化函数比较多,主要的初始化函数为下面这个。该函数初始化了websocket的url,并添加了开启websocket的手势。

- (instancetype)initWithToken:(NSString *)apiToken
                launchOptions:(NSDictionary *)launchOptions
                flushInterval:(NSUInteger)flushInterval
                 trackCrashes:(BOOL)trackCrashes
        automaticPushTracking:(BOOL)automaticPushTracking
               optOutTrackingByDefault:(BOOL)optOutTrackingByDefault
{
	......
	// switchboardURL为websocket连接的url地址
	self.switchboardURL = @"wss://switchboard.mixpanel.com";
	......
	// Mixpanel通过版本控制了是否支持可视化ABTest,v3.0.1之后的版本均支持,在后续代码中会看到关于enableVisualABTestAndCodeless的判断
#if defined(DISABLE_MIXPANEL_AB_DESIGNER) // Deprecated in v3.0.1
    self.enableVisualABTestAndCodeless = NO;
#else
    self.enableVisualABTestAndCodeless = YES;
#endif
	......	
	// setUpListeners会设置手势用于启动websocket服务
	[self setUpListeners];
	......
}

- (void)setUpListeners
{
	......
    [self initializeGestureRecognizer];
}

/**
 *	通过UILongPressGestureRecognizer手势开启websocket服务
 */
- (void)initializeGestureRecognizer
{
#if !MIXPANEL_NO_NOTIFICATION_AB_TEST_SUPPORT
    if (![Mixpanel isAppExtension]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.testDesignerGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                                                               action:@selector(connectGestureRecognized:)];
            self.testDesignerGestureRecognizer.minimumPressDuration = 3;
            self.testDesignerGestureRecognizer.cancelsTouchesInView = NO;
#if TARGET_IPHONE_SIMULATOR
            self.testDesignerGestureRecognizer.numberOfTouchesRequired = 2;
#else
            self.testDesignerGestureRecognizer.numberOfTouchesRequired = 4;
#endif
            // because this is in a dispatch_async, if the user sets enableVisualABTestAndCodeless in the first run
            // loop then this is initialized after that is set so we have to check here
            self.testDesignerGestureRecognizer.enabled = self.enableVisualABTestAndCodeless;
            [[Mixpanel sharedUIApplication].keyWindow addGestureRecognizer:self.testDesignerGestureRecognizer];
        });
    }
#endif // MIXPANEL_NO_NOTIFICATION_AB_TEST_SUPPORT
}

其中[Mixpanel sharedUIApplication]函数用于获取UIApplicaiton:

+ (UIApplication *)sharedUIApplication
{
    if ([[UIApplication class] respondsToSelector:@selector(sharedApplication)]) {
        return [[UIApplication class] performSelector:@selector(sharedApplication)];
    }
    return nil;
}

下面看看手势是如何启动websocket服务的,开启websocket的主要函数是connectToABTestDesigner:。该函数调用MPABTestDesignerConnection类进行websocket初始化,并传入了两个回调:connectCallback和disconnectCallback,分别表示连接成功和断开连接的回调。

- (void)connectGestureRecognized:(id)sender
{
	// 在Gesture的began开启websocket
    if (!sender || ([sender isKindOfClass:[UIGestureRecognizer class]] && ((UIGestureRecognizer *)sender).state == UIGestureRecognizerStateBegan)) {
        [self connectToABTestDesigner];
    }
}

- (void)connectToABTestDesigner
{
    [self connectToABTestDesigner:NO];
}

- (void)connectToABTestDesigner:(BOOL)reconnect
{
    // Ignore the gesture if the AB test designer is disabled.
    if (!self.enableVisualABTestAndCodeless) return;

    if ([self.abtestDesignerConnection isKindOfClass:[MPABTestDesignerConnection class]] && ((MPABTestDesignerConnection *)self.abtestDesignerConnection).connected) {
        MPLogWarning(@"A/B test designer connection already exists");
        return;
    }
    // 这里采用了简单的静态变量用于在block中修改oldInterval的值。
    static NSUInteger oldInterval;
    // 生成的websocket的url长这个样子
    // wss://switchboard.mixpanel.com/connect?key=f0e17d374dd97c9e364cdb319f8f839a&type=device
    NSString *designerURLString = [NSString stringWithFormat:@"%@/connect?key=%@&type=device", self.switchboardURL, self.apiToken];
    NSURL *designerURL = [NSURL URLWithString:designerURLString];
    __weak Mixpanel *weakSelf = self;
	
	// 定义连接成功回调
    void (^connectCallback)(void) = ^{
        __strong Mixpanel *strongSelf = weakSelf;
        oldInterval = strongSelf.flushInterval;
        // 成功连接,每一秒刷新一次
        strongSelf.flushInterval = 1;
        // 阻止iOS设备锁屏, 使用以下方式来保持屏幕一直开着
        [Mixpanel sharedUIApplication].idleTimerDisabled = YES;
        if (strongSelf) {
            // 停止所有的 MPVariant,待断开连接时再开启,因为连接状态会修改相应的内容
            for (MPVariant *variant in self.variants) {
                [variant stop];
            }
            // 停止所有的 MPEventBinding,待断开连接时再开启,因为连接状态会修改相应的内容
            for (MPEventBinding *binding in self.eventBindings) {
                [binding stop];
            }
            MPABTestDesignerConnection *connection = strongSelf.abtestDesignerConnection;
            void (^block)(id, SEL, NSString*, id) = ^(id obj, SEL sel, NSString *event_name, id params) {
                MPDesignerTrackMessage *message = [MPDesignerTrackMessage messageWithPayload:@{@"event_name": event_name}];
                [connection sendMessage:message];
            };

            [MPSwizzler swizzleSelector:@selector(track:properties:) onClass:[Mixpanel class] withBlock:block named:@"track_properties"];
        }
    };

	// 定义断开连接回调
    void (^disconnectCallback)(void) = ^{
        __strong Mixpanel *strongSelf = weakSelf;
        // 断开连接,回复之前的刷新速率
        strongSelf.flushInterval = oldInterval;
        [Mixpanel sharedUIApplication].idleTimerDisabled = NO;
        if (strongSelf) {
            // 重新开启所有的 MPVariant
            for (MPVariant *variant in self.variants) {
                [variant execute];
            }
            // 重新开启所有的 MPEventBinding
            for (MPEventBinding *binding in self.eventBindings) {
                [binding execute];
            }
            [MPSwizzler unswizzleSelector:@selector(track:properties:) onClass:[Mixpanel class] named:@"track_properties"];
        }
    };
    
    // 创建 websocket 连接对象 MPABTestDesignerConnection,传入连接成功回调和断开连接回调
    self.abtestDesignerConnection = [[MPABTestDesignerConnection alloc] initWithURL:designerURL
                                                                         keepTrying:reconnect
                                                                    connectCallback:connectCallback
                                                                 disconnectCallback:disconnectCallback];
}

连接成功和断开连接的地方都hook了一个方法@selector(track:properties:),该方法主要用来追踪属性,同时带上一些其他信息,如设备信息等


2. MPABTestDesignerConnection 管理 websocket

MPABTestDesignerConnection用来管理websocket,处理websocket开启、关闭以及数据的接收、发送。在MPABTestDesignerConnection的初始化函数主要处理了成员变量的初始化,其中关键两点是typeToMessageClassMap变量初始化和open:maxInterval:maxRetries:函数的调用。前者用于将服务器发来的请求映射到对应的处理类中,后者则开启websocket服务。

- (instancetype)initWithURL:(NSURL *)url keepTrying:(BOOL)keepTrying connectCallback:(void (^)(void))connectCallback disconnectCallback:(void (^)(void))disconnectCallback
{
    self = [super init];
    if (self) {
        // key 为request的字符穿,value用来映射服务器发送来的请求类
        _typeToMessageClassMap = @{
            MPABTestDesignerSnapshotRequestMessageType   : [MPABTestDesignerSnapshotRequestMessage class],
            MPABTestDesignerChangeRequestMessageType     : [MPABTestDesignerChangeRequestMessage class],
            MPABTestDesignerDeviceInfoRequestMessageType : [MPABTestDesignerDeviceInfoRequestMessage class],
            MPABTestDesignerTweakRequestMessageType      : [MPABTestDesignerTweakRequestMessage class],
            MPABTestDesignerClearRequestMessageType      : [MPABTestDesignerClearRequestMessage class],
            MPABTestDesignerDisconnectMessageType        : [MPABTestDesignerDisconnectMessage class],
            MPDesignerEventBindingRequestMessageType     : [MPDesignerEventBindingRequestMessage class],
        };

        _open = NO;
        _connected = NO;
        _sessionEnded = NO;
        _session = [NSMutableDictionary dictionary];
        _url = url;
        _connectCallback = connectCallback;
        _disconnectCallback = disconnectCallback;

        _commandQueue = [[NSOperationQueue alloc] init];
        _commandQueue.maxConcurrentOperationCount = 1;
        _commandQueue.suspended = YES;

		// 开启websocket服务
        if (keepTrying) {
            [self open:YES maxInterval:30 maxRetries:40];
        } else {
            [self open:YES maxInterval:0 maxRetries:0];
        }
    }

    return self;
}

open:maxInterval:maxRetries:开启socket服务,该函数处理两个问题,首次开启服务和socket连接断开后重新开启连接,并处理最大重试次数。

- (void)open:(BOOL)initiate maxInterval:(int)maxInterval maxRetries:(int)maxRetries
{
    static int retries = 0;
    BOOL inRetryLoop = retries > 0;

    MPLogDebug(@"In open. initiate = %d, retries = %d, maxRetries = %d, maxInterval = %d, connected = %d", initiate, retries, maxRetries, maxInterval, _connected);

    if (self.sessionEnded || _connected || (inRetryLoop && retries >= maxRetries) ) {
        // break out of retry loop if any of the success conditions are met.
        retries = 0;
    } else if (initiate ^ inRetryLoop) {
        // If we are initiating a new connection, or we are already in a
        // retry loop (but not both). Then open a socket.
        if (!_open) {
            MPLogDebug(@"Attempting to open WebSocket to: %@, try %d/%d ", _url, retries, maxRetries);
            _open = YES;
            _webSocket = [[MPWebSocket alloc] initWithURL:_url];
            _webSocket.delegate = self;
            [_webSocket open];
        }
        if (retries < maxRetries) {
            __weak MPABTestDesignerConnection *weakSelf = self;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(MIN(pow(1.4, retries), maxInterval) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                MPABTestDesignerConnection *strongSelf = weakSelf;
                [strongSelf open:NO maxInterval:maxInterval maxRetries:maxRetries];
            });
            retries++;
        }
    }
}

MPABTestDesignerConnection实现了接口MPWebSocketDelegate,MPWebSocketDelegate接口中定义了四个函数,用于处理websocket的状态

@protocol MPWebSocketDelegate <NSObject>

// message will either be an NSString if the server is using text
// or NSData if the server is using binary.
/**
 @description 接收websocket发送来的消息

 @param webSocket webSocket对象
 @param message server发送来的消息对象,可能是NSString或NSData
 */
- (void)webSocket:(MPWebSocket *)webSocket didReceiveMessage:(id)message;

@optional

- (void)webSocketDidOpen:(MPWebSocket *)webSocket;
- (void)webSocket:(MPWebSocket *)webSocket didFailWithError:(NSError *)error;
- (void)webSocket:(MPWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;

@end

前面说过MPABTestDesignerConnection初始化时有个关键属性_typeToMessageClassMap,map中的value均继承自MPAbstractABTestDesignerMessage抽象类,而抽象类又实现了MPABTestDesignerMessage协议。MPABTestDesignerMessage协议中定义了如何响应request,MPAbstractABTestDesignerMessage抽象类中实现了简单的数据存储,抽象类的子类则完成了对具体request的响应。用一个简单类图可以表示为:
Mixpanel 可视化ABTest分析 —— iOS篇
接下来通过socket的回调来梳理消息的处理的流程。在webSocket:didReceiveMessage:获取消息时,首先处理了连接成功的回调,然后根据message消息创建id<MPABTestDesignerMessage>类型对象,接着通过id<MPABTestDesignerMessage>对象获取需要返回的消息,并生成NSOperation对象,最后将NSOperation对象添加到NSOperationQueue中运行

- (void)webSocket:(MPWebSocket *)webSocket didReceiveMessage:(id)message
{
	// 处理首次成功接收message消息才处理连接成功的回调
    if (!_connected) {
        _connected = YES;
        [self showConnectedViewWithLoading:NO];
        if (_connectCallback) {
            _connectCallback();
        }
    }
    
    // 根据消息生成相应的消息处理对象
    id<MPABTestDesignerMessage> designerMessage = [self designerMessageForMessage:message];
    MPLogInfo(@"WebSocket received message: %@", [designerMessage debugDescription]);

	// 处理消息
    NSOperation *commandOperation = [designerMessage responseCommandWithConnection:self];
    if (commandOperation) {
        [_commandQueue addOperation:commandOperation];
    }
}

在收到的message中通过type得到_typeToMessageClassMap字典中的key值,然后获取对应的value message

- (id <MPABTestDesignerMessage>)designerMessageForMessage:(id)message
{
    MPLogInfo(@"raw message: %@", message);
    
	// 仅处理 NSString 和 NSData 类型
    NSParameterAssert([message isKindOfClass:[NSString class]] || [message isKindOfClass:[NSData class]]);

    id <MPABTestDesignerMessage> designerMessage = nil;

	// 转化类型,统一处理
    NSData *jsonData = [message isKindOfClass:[NSString class]] ? [(NSString *)message dataUsingEncoding:NSUTF8StringEncoding] : message;

    NSError *error = nil;
    id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:(NSJSONReadingOptions)0 error:&error];
    if ([jsonObject isKindOfClass:[NSDictionary class]]) {
        NSDictionary *messageDictionary = (NSDictionary *)jsonObject;
        // 通过@"type"获取 _typeToMessageClassMap 中的 key 值,以便找到对应的处理消息的对象
        NSString *type = messageDictionary[@"type"];
        // "payload"中包含了其他配置参数
        NSDictionary *payload = messageDictionary[@"payload"];

        designerMessage = [_typeToMessageClassMap[type] messageWithType:type payload:payload];
    } else {
        MPLogWarning(@"Badly formed socket message expected JSON dictionary: %@", error);
    }

    return designerMessage;
}

接下来重点来了,我们来分析下继承了MPAbstractABTestDesignerMessage类的各子类的具体实现,它们是处理服务器请求的关键类。

待续。。。