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
ios无痕埋点_无痕埋点方案探究相关推荐
- python 埋点_数据埋点方案简述
数据是机器学习的前提,前面使用Python爬虫抓取数据篇介绍了通过爬虫抓取网页的方式采集数据.对于新产品,最重要的事项是获取用户,参看前面互联网产品怎么发掘种子用户和意见领袖 这篇. 在产品上线之后, ...
- ios无痕埋点_移动端无痕埋点实践详解(二)
0x01 前言 在移动端无痕埋点实践详解(一)这篇文章大致总结了移动端无痕埋点的基本原理.主要介绍了什么是无痕埋点,无痕埋点的基础数据流程以及在Android系统上总体思路.这篇文章着重总结下无痕埋点 ...
- ios无痕埋点_iOS可视化埋点方案
前言 随着公司业务的发展,数据的重要性日益体现出来. 数据埋点的准确和全面性显得尤为重要. 通过精准和详细的数据,后面的分析才有意义.随着业务的不断变化,动态化埋点也越来越重要. 三大埋点方式 为了解 ...
- 手动埋点转无痕埋点,如何做到代码“零”入侵
前言 在起初的手动埋点的时候,每次版本大更新,很多埋点都要进行修改,删除.这个时候之前嵌在源码里面的一行行埋点代码要进行修改,删除.删了又找,找了又改,很麻烦.如果遇到有代码洁癖的,"产品你 ...
- 前端埋点的缺点_【埋点学习埋点质量】埋点的框架设计及其准确性
是新朋友吗?记得先点蓝字关注我哦- 通过前两章<送你一份埋点需求分析&设计埋点方案><一份规范的埋点需求文档该如何写?>,我们已经足够了解埋点,并且能够输出埋点文档了. ...
- h5 神策埋点_数据分析(一)埋点
1. 埋点是什么? 埋点是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获.处理和发送的相关技术及其实施过程.比如用户某个按钮点击次数.浏览某个一刻吗时长 ...
- api可以主动采集用户数据吗_数据埋点采集的那些事儿
数据采集是数据分析的基础,而埋点是最主要的采集方式.那么数据埋点采集到底都是哪些事呢?我们主要从三个方面来看:什么是埋点,埋点怎么设计,埋点的应用. 一.数据采集以及常见数据问题 1.1数据采集 数据 ...
- h5 神策埋点_咕咚技术总监唐平麟:神策使我们的数据平台成本降低约 75%,迭代效率提升 2~3 倍...
在这个数据爆炸的时代,数据成为各行各业出奇制胜的法宝,运动行业也不例外,那么大数据对运动业有什么价值呢? 咕咚作为智能运动的倡导者和先行者,致力于成为全球领先的运动大数据和服务平台,现已为超过 1.5 ...
- python 埋点_网站js埋点
js埋点 1.埋点作用: 页面埋点的作用:其实就是用于流量分析.而流量的意思,包含了很多:页面浏览数(PV).独立访问者数量(UV).IP.页面停留时间.页面操作时间.页面访问次数.按钮点击次数.文件 ...
- 埋点 神策小程序_神策埋点思路
数据模型的建立 神策的基本事件模型包括事件(Event)和用户(User)两个:比如说要统计今天注册了tcl会员小程序的人数.区分是注册事件还是别的事件,用到了事件的模型.每个用户启动N次只能算一次, ...
最新文章
- 生产者与消费者-1:N-基于list
- Vue-第七天 学习与相关问题总结
- 辽宁科技大学计算机好调剂吗,2020年辽宁科技大学硕士研究生调剂办法
- laravel session redis 设置
- Python发送文本邮件
- git clone报错:fatal: unable to access ‘https://github.com/...
- CornerNet: 将目标检测问题视作关键点检测与配对
- Android绑定服务后出现空指针异常问题
- Python语言实现用requests和正则表达式方法爬取猫眼电影排行榜前100部电影
- github克隆仓库加速
- 安装vue脚手架出现的问题 npm ERR! code EEXIST。。。
- 读书笔记:普通心理学之个体心理
- AE制作文字模糊特效
- python爬京东优惠券_京东抽奖爬虫LiteVersion
- xxd航模电调电路图
- 软件项目管理MOOC(北邮)——第七章测试答案
- VisionMobile 2012年移动开发者经济报告(九) 四 应用市场销售(下)
- 2018年全国多校算法寒假训练营练习比赛(第五场)解题报告
- 【2020年第二届“网鼎杯”网络安全大赛 青龙组】Web AreUSerialz
- 03.规格及模板管理
热门文章
- python打印星号组成的三角形_Python利用for循环打印星号三角形的案例
- Win10系统更新显卡驱动无限蓝屏重启-驱动人生解决方案
- 电脑中毒,文件夹都变成exe文件怎么办?
- win7访问smb文件服务器,win7系统配置smb服务器
- NetLimiter 4.0.15.0 x64 破解新鲜出炉!
- 中国式家长计算机怎么学,中国式家长开局学习技巧详解 大神教你如何完美开局...
- python怎么加逗号_Python 逗号的巧用
- 从零开始学习Openwrt教程
- Allegro PCB操作技巧
- 展览 | 2018届中国国际信息通信展览的所见所闻