背景
业务扩展的需要,对用户行为数据的收集和分析也就日益重要,前期实现的打点方案只能使用在单一app客户端中,无法移植跨app使用。故安领导要求,我和一名同事接手了iOS客户端统计打点sdk化的任务。要求完成时间:6个工作日!!!!
业务分析
打点类型
- pageView打点,页面主要业务数据展示成果后提交的打点
- pageClick打点,用户点击事件产生的用户行为打点
- pageExchange打点,打开新的页面窗口,纪录页面流转的打点
- exposure打点,用户浏览时,曝光产品的打点
行为与业务数据
打点纪录的不仅仅是用户的操作行为,还需要涉及到具体用户操作的业务数据。比如,用户点击了收藏,则打点事件中应该有收藏的物品的ID或者其他属性等。用户浏览一段商品列表,曝光中应该有具体商品的信息上报,如ID,商品类型,商品sku等。
原先sdk的设计原则是要脱离业务数据的,但是打点的核心就是上报该有的业务数据。所以最后决定使用业务数据埋点的方式解决这个问题。
业务数据埋点
将业务数据和界面元素绑定,形成一个包含业务数据的独特视图。技术上实现可以扩展了展示视图的属性。
如view上面扩展一个analysisData的属性,在这个view生成的时候,定义一份业务数据赋予analysisData。当这个view有用户操作产生打点的时候,则取analysisData作为业务数据解析上传。针对可复用的视图类型,则需要有搭配的数据源,保证复用后取到的业务数据是用户操作的界面元素对应的业务数据。
需要客户端业务方手动赋予业务数据
业务数据的获取和赋值,手动赋予无法避免。
比如,一个收藏按钮需要打点,那么开发这个按钮点击事件的同学,需要按sdk的规范给该按钮的扩展属性赋值,那么在sdk打点时,才能有业务数据上报。没有业务数据的用户操作默认为无打点事件。
模块设计
抓取模块
- 按钮点击事件的抓取
- 手势事件的抓取
- tableView和collectionView代理事件的抓取
打点数据收集模块
抓取模块可以抓取到大部分用户的行为操作,收集模块负责将这些行为按统计需求进行特殊的数据结构处理处理。
打点数据存储模块
将收集模块数据结构处理好的打点数据,构建每项打点数据之间的用户行为关联,形成可以分析用户行为的打点数据链,并进行存储。
打点数据上报模块
负责连续,不遗漏,安全,不影响用户使用app的前提下,上报打点数据。此处进行最后封装和数据加密。
打点配置解析模块
请求后台的打点配置规则,解析成sdk打点的使用规则,如可以动态配置获取业务数据的类型,配置上报的规则和地址等
打点sdk主要由以上五个模块组成
抓取模块技术实现
实现按钮点击事件的抓取
方法:扩展UIControl
+ (void)load {
[self swizzleInstanceMethod:@selector(sendAction:to:forEvent:) with:@selector(mySendAction:to:forEvent:)];
}
- (void)mySendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event {
[self mySendAction:action to:target forEvent:event];
[[StatisticInterceptionManager sharedInstance] control:self sendAction:action to:target forEvent:event];
}
实现手势事件的抓取
方法:扩展UITapGestureRecognizer
+ (void)load {
[self swizzleInstanceMethod:@selector(initWithTarget:action:) with:@selector(myInitWithTarget:action:)];
[self swizzleInstanceMethod:@selector(addTarget:action:) with:@selector(myAddTarget:action:)];
}
- (instancetype)myInitWithTarget:(nullable id)target action:(nullable SEL)action {
id instance = [self myInitWithTarget:target action:action];
[instance myAddTarget:[BZMStatisticInterceptionManager sharedInstance] action:@selector(tapGestureRecognizerDidTap:)];
return instance;
}
- (void)myAddTarget:(id)target action:(SEL)action {
[self myAddTarget:target action:action];
[self myAddTarget:[BZMStatisticInterceptionManager sharedInstance] action:@selector(tapGestureRecognizerDidTap:)];
}
tableView和collectionView代理事件的抓取
方法:扩展对应类,交换setDelegate:方法
+ (void)load {
[self swizzleInstanceMethod:@selector(setDelegate:) with:@selector(setMyDelegate:)];
}
- (void)setMyDelegate:(id<UITableViewDelegate>)delegate {
if (!delegate) {
[self setMyDelegate:nil];
return;
}
UITableViewDelegateForwarder *delegateForwarder = [[UITableViewDelegateForwarder alloc] init];
delegateForwarder.delegate = delegate;
self.delegateForward = delegateForwarder;
[self setMyDelegate:nil];
[self setMyDelegate:delegateForwarder];
}
- (void)setDelegateForward:(UITableViewDelegateForwarder *)delegateForward {
objc_setAssociatedObject(self, @selector(delegateForward), delegateForward, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UITableViewDelegateForwarder *)delegateForward {
return objc_getAssociatedObject(self, @selector(delegateForward));
}
UITableViewDelegateForwarder内的方法实现
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
if([_delegate respondsToSelector:selector])
{
[invocation invokeWithTarget:_delegate];
BZMStatisticInterceptionManager *sd = [StatisticInterceptionManager sharedInstance];
if ([sd respondsToSelector:selector]) {
[invocation invokeWithTarget:sd];
}
}
}
- (BOOL)respondsToSelector:(SEL)selector
{
return [_delegate respondsToSelector:selector];
}
- (id)methodSignatureForSelector:(SEL)selector
{
return [(NSObject *)_delegate methodSignatureForSelector:selector];
}
collectionView的响应处理和tableView类似
+ (void)load {
[self swizzleInstanceMethod:@selector(setDelegate:) with:@selector(setMyDelegate:)];
}
- (void)setMyDelegate:(id<UICollectionViewDelegate>)delegate {
[self setDelegateForward:nil];
if (!delegate) {
[self setMyDelegate:nil];
return;
}
UICollectionViewDelegateForwarder *delegateForwarder = [[UICollectionViewDelegateForwarder alloc] init];
delegateForwarder.delegate = delegate;
[self setDelegateForward:delegateForwarder];
[self setMyDelegate:nil];
[self setMyDelegate:delegateForwarder];
}
- (void)setDelegateForward:(UICollectionViewDelegateForwarder *)delegateForward {
objc_setAssociatedObject(self, @selector(delegateForward), delegateForward, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UICollectionViewDelegateForwarder *)delegateForward {
return objc_getAssociatedObject(self, @selector(delegateForward));
}
UICollectionViewDelegateForwarder内的方法实现
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
if([_delegate respondsToSelector:selector])
{
[invocation invokeWithTarget:_delegate];
BZMStatisticInterceptionManager *sd = [StatisticInterceptionManager sharedInstance];
if ([sd respondsToSelector:selector]) {
[invocation invokeWithTarget:sd];
}
}
}
- (BOOL)respondsToSelector:(SEL)selector
{
BOOL resule = [_delegate respondsToSelector:selector];
return resule;
}
- (id)methodSignatureForSelector:(SEL)selector
{
return [(NSObject *)_delegate methodSignatureForSelector:selector];
}
业务上导致的技术难点
业务需求上,打点的信息中,需要包含页面的来源:refer。如A页面一个按钮点击打开了B页面,这个时候产生了一个pageExchange,在B页面主要业务数据出来之后产生一个pageView。
B页面的pageClick和曝光等打点都需要纪录refer这个属性。
1 使用堆栈纪录页面的变化,保证栈顶是最新的refer(涉及到入栈出栈的业务上导致的更新逻辑比较负责,未使用,如果客户端代码规范比较统一,这个方法比较简单)
2 当产生一次页面变化时,将refer存在当前的controller及其parentController的扩展属性上,且维护一个静态变量lastPageExchange存储最新的pageExchange。根据业务情况来更新lastPageExchange和controller上的refer,保证当前拿到的refer都是打开这个页面的上个页面的信息。(暂且使用该方法能兼容当前业务需求)
-------------------------------分割线-------------------------------
2017年3月7日,使用Aspects发现会有很多截取崩溃,故修改手势截取的处理办法,在iinitWithTarget:action:和addTarget:action:的时候,额外增加一个抓取处理的addTarget:action:。
-------------------------------分割线-------------------------------
2017年3月14日,在调起系统相册运用时,涉及到collectionview的一些代理触发,发现dealloc,delegate置成nil之后还会被响应,导致无法找到对应代理方法而崩溃,暂未发现具体导致的原因。容错处理,当该情况发生时,准备了一个实现了所有tableview和collectionview代理的容错类,去实现这个不该存在的delegate。
-------------------------------分割线-------------------------------
2017年4月初,发现渠道包中,有比较多的打点导致的崩溃,大部分是IndexPath或是参数丢失导致,也就是说截取方法的时候,某些参数在NSInvocation往下传递的时候已经被释放了,所以在- (void)forwardInvocation:(NSInvocation *)invocation函数开始调用[invocation retainArguments];让NSInvocation对自己使用到的参数retain一次,具体解决情况还在跟进,从正式包的崩溃日志来看,没有发现参数释放导致的崩溃了。