无痕埋点方案探究

目前埋点的设计大致有以下几种:
参考 网易HubbleData无埋点SDK在iOS端的设计与实现

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

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

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

可视化埋点

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


3104472-15d0364de7f22ecd.png

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

唯一标识问题

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


3104472-3b1942be410c1e02.jpeg

这里主要分为两个部分 :

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

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

整体代码示例

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

1、UIViewController PV统计

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

@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来标记,具体代码如下:

~~~ 
@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 方法来实现, 具体的原理是 具体代码如下:

@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:);
1
// 初始化一个名字为 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来锁定的, 代码如下:

@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:)];
1
}); 
}

(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"];
    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 event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", identifier, eventid, targetname, pageid, pagename, uploadDic);

}
} 
@end 

配置表结构

首先那, 配置表是一个json数据。 针对不同的场景 (UIControl , 页面PV, Tabeview, Gesture)都做了区分, 用不同的key区别。 对于 “固定参数” , 我们之间写到配置表中,而对于业务参数, 我们之间写清楚参数在业务内的名字, 以及上传时的 keyName, 参数的持有者。 通过Runtime + KVC来取值。 配置表可以是这个样子:(仅供参考)

{
    "ACTION": {
        "ViewController/jumpSecond": {
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "234",
                "pagename": "button点击,跳转至下一个页面"
            },
            "pagePara": {
                "testKey9": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        },
        
        "SecondViewController/back": {
            "userDefined": {
                "eventid": "201803074|965",
                "target": "second",
                "pageid": "234",
                "pagename": "button点击,返回"
            },
            "pagePara": {
                "testKey9": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    },
    
    "PAGEPV": {
        "ViewController": {
            "userDefined": {
                "pageid": "234",
                "pagename": "XXX 页面展示了"
            },
            "pagePara": {
                "testKey10": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        },
        "SecondViewController": {
            "userDefined": {
                "pageid": "234",
                "pagename": "XXX页面展示"
            },
            "pagePara": {
                "testKey0": {
                    "propertyName": "age",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    },
    "TABLEVIEW": {
        "ViewController/TestTableview/0":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "234",
                "pagename": "tableview 被点击"
            },
            "pagePara": {
                "user_grade": {
                    "propertyName": "grade",
                    "propertyPath":"",
                    "containIn": "1"
                }
            }
        }
    },
    
    "GESTURE": {
        "ViewController/gesture1clicked:":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "手势1对应的id",
                "pagename": "手势1对应的page name"
            },
            "pagePara": {
                "testKey1": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
                
            }
        },
        "ViewController/gesture2clicked:":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "手势2对应的id",
                "pagename": "手势2对应的page name"
            },
            "pagePara": {
                "testKey2": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
                
            }
        },
        
        "SecondViewController/gesture3clicked:":{
            "userDefined": {
                "eventid": "201803074|98",
                "target": "",
                "pageid": "gesture3clicked",
                "pagename": "手势3对应的page name"
            },
            "pagePara": {
                "user_age": {
                    "propertyName": "goodsnumber",
                    "propertyPath":"",
                }
                
            }
        }
    }
}

取参方法


#import "CaptureTool.h"
#import 

@implementation CaptureTool

+(id)captureVarforInstance:(id)instance varName:(NSString *)varName
{
    id value = [instance valueForKey:varName];

    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([instance class], &count);
    
    if (!value) {
        NSMutableArray * varNameArray = [NSMutableArray arrayWithCapacity:0];
        for (int i = 0; i < count; i++) {
            objc_property_t property = properties[i];
            NSString* propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];
            NSArray* splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""];
            if (splitPropertyAttributes.count < 2) {
                continue;
            }
            NSString * className = [splitPropertyAttributes objectAtIndex:1];
            Class cls = NSClassFromString(className);
            NSBundle *bundle2 = [NSBundle bundleForClass:cls];
            if (bundle2 == [NSBundle mainBundle]) {
//                NSLog(@"自定义的类----- %@", className);
                const char * name = property_getName(property);
                NSString * varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding];
                [varNameArray addObject:varname];
            } else {
//                NSLog(@"系统的类");
            }
        }
        
        for (NSString * name in varNameArray) {
            id newValue = [instance valueForKey:name];
            if (newValue) {
                value = [newValue valueForKey:varName];
                if (value) {
                    return value;
                }else{
                    value = [[self class] captureVarforInstance:newValue varName:varName];
                }
            }
        }
    }
    return value;
}


+(id)captureVarforInstance:(id)instance withPara:(NSDictionary *)para
{
    NSString * properyName = para[@"propertyName"];
    // 实例中包含其他对象的情况
    NSString * propertyPath = para[@"propertyPath"];
    if (propertyPath.length > 0) {
        NSArray * keysArray = [propertyPath componentsSeparatedByString:@"/"];
     
        return [[self class] captureVarforInstance:instance withKeys:keysArray];
    }
    return [[self class] captureVarforInstance:instance varName:properyName];
}

+(id)captureVarforInstance:(id)instance withKeys:(NSArray *)keyArray
{
    id result = [instance valueForKey:keyArray[0]];
    
    if (keyArray.count > 1 && result) {
        int i = 1;
        while (i < keyArray.count && result) {
            result = [result valueForKey:keyArray[i]];
            i++;
        }
    }
    return result;
}

@end

——–项目github地址

你可能感兴趣的:(无痕埋点方案探究)