AOP(面向切面编程)iOS Developer埋点操作

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

2017-03-04  本文已影响1380人  littlewish

背景

业务扩展的需要,对用户行为数据的收集和分析也就日益重要,前期实现的打点方案只能使用在单一app客户端中,无法移植跨app使用。故安领导要求,我和一名同事接手了iOS客户端统计打点sdk化的任务。要求完成时间:6个工作日!!!!

业务分析

打点类型
行为与业务数据

打点纪录的不仅仅是用户的操作行为,还需要涉及到具体用户操作的业务数据。比如,用户点击了收藏,则打点事件中应该有收藏的物品的ID或者其他属性等。用户浏览一段商品列表,曝光中应该有具体商品的信息上报,如ID,商品类型,商品sku等。

原先sdk的设计原则是要脱离业务数据的,但是打点的核心就是上报该有的业务数据。所以最后决定使用业务数据埋点的方式解决这个问题。

业务数据埋点

将业务数据和界面元素绑定,形成一个包含业务数据的独特视图。技术上实现可以扩展了展示视图的属性。

如view上面扩展一个analysisData的属性,在这个view生成的时候,定义一份业务数据赋予analysisData。当这个view有用户操作产生打点的时候,则取analysisData作为业务数据解析上传。针对可复用的视图类型,则需要有搭配的数据源,保证复用后取到的业务数据是用户操作的界面元素对应的业务数据。

需要客户端业务方手动赋予业务数据

业务数据的获取和赋值,手动赋予无法避免。

比如,一个收藏按钮需要打点,那么开发这个按钮点击事件的同学,需要按sdk的规范给该按钮的扩展属性赋值,那么在sdk打点时,才能有业务数据上报。没有业务数据的用户操作默认为无打点事件。

模块设计

抓取模块
打点数据收集模块

抓取模块可以抓取到大部分用户的行为操作,收集模块负责将这些行为按统计需求进行特殊的数据结构处理处理。

打点数据存储模块

将收集模块数据结构处理好的打点数据,构建每项打点数据之间的用户行为关联,形成可以分析用户行为的打点数据链,并进行存储。

打点数据上报模块

负责连续,不遗漏,安全,不影响用户使用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一次,具体解决情况还在跟进,从正式包的崩溃日志来看,没有发现参数释放导致的崩溃了。

上一篇下一篇

猜你喜欢

热点阅读