登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

iOS无痕埋点方案分享探究 [复制链接]

2018-12-10 10:23
hellokenken 阅读:2329 评论:0 赞:0
Tag:  

前言

当前互联网行业的竞争已经是非常激烈了, “功能驱动”的时代已经过去了, 现在更加注重软件的细节, 以及用户的体验问题。 说到用户体验,就不得不提到用户的操作行为。 在我们的软件中,我们会到处进行埋点, 以便提取到我们想要的数据,进而分析用户的行为习惯。 通过这些数据,我们也可以更好的分析出用户的操作趋势,从而在用户体验上把我们的app做的更好。

随着公司业务的发展,数据的重要性日益体现出来。 数据埋点的全面性和准确性尤为重要。 只有拿到精准并详细的数据, 后面的分析才有意义。 然后随着业务的不断变化, 埋点的动态性也越来越重要。为了解决这些问题, 很多公司都提出自己的解决方案, 各中解决方案中,大体分为以下三种:

  1. 代码埋点

    由开发人员在触发事件的具体方法里,植入多行代码把需要上传的参数上报至服务端。

  2. 可视化埋点

    根据标识来识别每一个事件, 针对指定的事件进行取参埋点。而事件的标识与参数信息都写在配置表中,通过动态下发配置表来实现埋点统计。

  3. 无埋点

    无埋点并不是不需要埋点,更准确的说应该是“全埋”, 前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据, 并生成可视化报告供专业人员分析 , 因此实现“无埋点”统计。


由于考虑到“无埋点”的方案成本较高,并且后期解析也比较复杂,加上view_path的不确定性(具体可以参考: 网易HubbleData无埋点SDK在iOS端的设计与实现)。所以本文重点分享一个 可视化埋点 的简单实现方式。

可视化埋点

首先,可视化埋点并非完全抛弃了代码埋点,而是在代码埋点的上层封装的一套逻辑来代替手工埋点,大体上架构如下图:

image.png

不过要实现可视化埋点也有很多问题需要解决,比如事件唯一标识的确定,业务参数的获取,有逻辑判断的埋点配置项信息等等。接下来我会重点围绕唯一标识以及业务参数获取这两个问题给出自己的一个解决方案。

唯一标识问题

唯一标识的组成方式主要是又 target + action 来确定, 即任何一个事件都存在一个target与action。 在此引入AOP编程,AOP(Aspect-Oriented-Programming)即面向切面编程的思想,基于 Runtime 的 Method Swizzling能力,来 hook 相应的方法,从而在hook方法中进行统一的埋点处理。例如所有的按钮被点击时,都会触发UIApplication的sendAction方法,我们hook这个方法,即可拦截所有按钮的点击事件。

image.png

这里主要分为两个部分 :

  • 事件的锁定

    事件的锁定主要是靠 “事件唯一标识符”来锁定,而事件的唯一标识是由我们写入配置表中的。

  • 埋点数据的上报。

    埋点数据的数据又分为两种类型: 固定数据与可变的业务数据, 而固定数据我们可以直接写到配置表中, 通过唯一标识来获取。而对于业务数据,我是这么理解的: 数据是有持有者的, 例如我们Controller的一个属性值, 又或者数据再Model的某一个层级。 这么的话我们就可以通过KVC的的方式来递归获取该属性的值来取到业务数据, 代码后面会有介绍。

整体代码示例

由于iOS中的事件场景是多样的, 在此我以UIControl, UITablview(collectionView与tableView基本相同), UITapGesture, UIViewController的PV统计 为例,介绍一下具体思路。

1. UIViewController PV统计

页面的统计较为简单,利用Method Swizzing hook 系统的viewDidLoad,  直接通过页面名称即可锁定页面的展示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@implementation UIViewController (Analysis)
 
+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
     
        SEL originalDidLoadSelector = @selector(viewDidLoad);
        SEL swizzingDidLoadSelector = @selector(user_viewDidLoad);
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];
         
    });
}
 
-(void)user_viewDidLoad
{
    [self user_viewDidLoad];
     
   //从配置表中取参数的过程 1 固定参数  2 业务参数(此处参数被target持有)
    NSString * identifier = [NSString stringWithFormat:@"%@", [self class]];
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier];
    if (dic) {
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
         
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
             
            id value = [CaptureTool captureVarforInstance:self withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
         
        NSLog(@"\n 事件唯一标识为:%@ \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", [self class], pageid, pagename, uploadDic);
    }
}

2. UIControl 点击统计。

主要通过hook  sendAction:to:forEvent: 来实现, 其唯一标识符我们用 targetname/selector/tag来标记,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@implementation UIControl (Analysis)
 
+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzingSelector = @selector(user_sendAction:to:forEvent:);
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
    });
}
 
 
-(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    [self user_sendAction:action to:target forEvent:event];
     
    NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [target class], NSStringFromSelector(action),self.tag];
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"ACTION"] objectForKey:identifier];
    if (dic) {
         
        NSString * eventid = dic[@"userDefined"][@"eventid"];
        NSString * targetname = dic[@"userDefined"][@"target"];
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
             
            id value = [CaptureTool captureVarforInstance:target withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
         
         
        NSLog(@" \n  唯一标识符为 : %@, \n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", identifier, eventid, targetname, pageid, pagename, uploadDic);
    }
}

3. TableView (CollectionView) 的点击统计。

tablview的唯一标识, 我们使用 delegate.class/tableview.class/tableview.tag的组合来唯一锁定。 主要是通过hook setDelegate 方法, 在设置代理的时候再去交互 didSelect 方法来实现,  具体的原理是 具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@implementation UITableView (Analysis)
 
+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
     
        SEL originalAppearSelector = @selector(setDelegate:);
        SEL swizzingAppearSelector = @selector(user_setDelegate:);
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
    });
}
 
 
-(void)user_setDelegate:(id)delegate
{
    [self user_setDelegate:delegate];
     
    SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
     
// 初始化一个名字为  delegate.class/tableview.class/tableview.tag 的selector
    SEL sel_ =  NSSelectorFromString([NSString stringWithFormat:@"%@/%ld", [self class], self.tag]);
     
// 将生成的selector的方法 加入的 delegate类中, 并且该方法的实现(IMP)指向当前类user_tableView:didSelectRowAtIndexPath: 方法的实现
    class_addMethod([delegate class],
                    sel_,
                    method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))),
                    nil);
     
     
    //判断是否有实现,没有的话添加一个实现
    if (![self isContainSel:sel inClass:[delegate class]]) {
        IMP imp = method_getImplementation(class_getInstanceMethod([delegate class], sel));
        class_addMethod([delegate class], sel, imp, nil);
    }
     
     
    // 将swizzle delegate method 和 origin delegate method 交换
    [MethodSwizzingTool swizzingForClass:[delegate class] originalSel:sel swizzingSel:sel_];
}
//判断页面是否实现了某个sel
- (BOOL)isContainSel:(SEL)sel inClass:(Class)class {
    unsigned int count;
     
    Method *methodList = class_copyMethodList(class,&count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
        if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) {
            return YES;
        }
    }
    return NO;
}
// 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法:
-(void)user_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
     
//通过唯一标识的规则, 找到原来的方法 (即tableView:didSelectRowAtIndexPath: 方法)
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@/%ld", [tableView class], tableView.tag]);
    if ([self respondsToSelector:sel]) {
//以下是对方法的调用以及传参,performSelector 方法底层实现与此相似
        IMP imp = [self methodForSelector:sel];
        void (*func)(id, SEL,id,id) = (void *)imp;
        func(self, sel,tableView,indexPath);
    }
     
    //配置表中, 事件唯一标识即为key, 通过key 取value, 取到了就说明该事件配置的有埋点上传
    NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [self class],[tableView class], tableView.tag];
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"TABLEVIEW"] objectForKey:identifier];
    if (dic) {
         
        NSString * eventid = dic[@"userDefined"][@"eventid"];
        NSString * targetname = dic[@"userDefined"][@"target"];
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
                 
        UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            NSInteger containIn = [obj[@"containIn"] integerValue];
            //通过containIn 参数判断数据持有者,后续会有解释
            id instance = containIn == 0 ? self : cell;
            id value = [CaptureTool captureVarforInstance:instance withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
         
        NSLog(@"\n 事件的唯一标识为 %@, \n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", identifier,  eventid, targetname, pageid, pagename, uploadDic);
    }
}

4. gesture方式添加的的点击统计。

gesture的事件,是通过 hook initWithTarget:action:方法来实现的, 事件的唯一标识依然是target.class/actionname来锁定的, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@implementation UIGestureRecognizer (Analysis)
 
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
         
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(initWithTarget:action:) swizzingSel:@selector(vi_initWithTarget:action:)];
    });
}
 
- (instancetype)vi_initWithTarget:(nullable id)target action:(nullable SEL)action
{
    UIGestureRecognizer *selfGestureRecognizer = [self vi_initWithTarget:target action:action];
     
    if (!target && !action) {
        return selfGestureRecognizer;
    }
     
    if ([target isKindOfClass:[UIScrollView class]]) {
        return selfGestureRecognizer;
    }
     
    Class class = [target class];
     
     
    SEL originalSEL = action;
     
    NSString * sel_name = [NSString stringWithFormat:@"%s/%@", class_getName([target class]),NSStringFromSelector(action)];
    SEL swizzledSEL =  NSSelectorFromString(sel_name);
     
    BOOL isAddMethod = class_addMethod(class,
                                       swizzledSEL,
                                       method_getImplementation(class_getInstanceMethod([self class], @selector(responseUser_gesture:))),
                                       nil);
     
    if (isAddMethod) {
        [MethodSwizzingTool swizzingForClass:class originalSel:originalSEL swizzingSel:swizzledSEL];
    }
     
    self.name = NSStringFromSelector(action);
    return selfGestureRecognizer;
}
-(void)responseUser_gesture:(UIGestureRecognizer *)gesture
{
     
    NSString * identifier = [NSString stringWithFormat:@"%s/%@", class_getName([self class]),gesture.name];
     
    SEL sel = NSSelectorFromString(identifier);
    if ([self respondsToSelector:sel]) {
        IMP imp = [self methodForSelector:sel];
        void (*func)(id, SEL,id) = (void *)imp;
        func(self, sel,gesture);
    }
     
     
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"GESTURE"] objectForKey:identifier];
    if (dic) {
         
        NSString * eventid = dic[@"userDefined"][@"eventid"];
        NSString * targetname = dic[@"userDefined"][@"target"];
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];