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的响应。用一个简单类图可以表示为:
接下来通过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类的各子类的具体实现,它们是处理服务器请求的关键类。
待续。。。