iOS客户端统计打点sdk设计心得

背景

业务扩展的需要,对用户行为数据的收集和分析也就日益重要,前期实现的打点方案只能使用在单一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一次,具体解决情况还在跟进,从正式包的崩溃日志来看,没有发现参数释放导致的崩溃了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容