目前埋點(diǎn)的設(shè)計(jì)大致有以下幾種:
參考 網(wǎng)易HubbleData無(wú)埋點(diǎn)SDK在iOS端的設(shè)計(jì)與實(shí)現(xiàn)
1铅协、代碼埋點(diǎn)
由開(kāi)發(fā)人員在觸發(fā)事件的具體方法里胸懈,植入多行代碼把需要上傳的參數(shù)上報(bào)至服務(wù)端更卒。
2、可視化埋點(diǎn)
根據(jù)標(biāo)識(shí)來(lái)識(shí)別每一個(gè)事件吨娜, 針對(duì)指定的事件進(jìn)行取參埋點(diǎn)问拘。而事件的標(biāo)識(shí)與參數(shù)信息都寫(xiě)在配置表中,通過(guò)動(dòng)態(tài)下發(fā)配置表來(lái)實(shí)現(xiàn)埋點(diǎn)統(tǒng)計(jì)压鉴。
3崖咨、無(wú)埋點(diǎn)
無(wú)埋點(diǎn)并不是不需要埋點(diǎn),更準(zhǔn)確的說(shuō)應(yīng)該是“全埋”油吭, 前端的任意一個(gè)事件都被綁定一個(gè)標(biāo)識(shí)击蹲,所有的事件都別記錄下來(lái)。 通過(guò)定期上傳記錄文件婉宰,配合文件解析歌豺,解析出來(lái)我們想要的數(shù)據(jù), 并生成可視化報(bào)告供專(zhuān)業(yè)人員分析 心包, 因此實(shí)現(xiàn)“無(wú)埋點(diǎn)”統(tǒng)計(jì)类咧。
可視化埋點(diǎn)
首先,可視化埋點(diǎn)并非完全拋棄了代碼埋點(diǎn)蟹腾,而是在代碼埋點(diǎn)的上層封裝的一套邏輯來(lái)代替手工埋點(diǎn)痕惋,大體上架構(gòu)如下圖:
不過(guò)要實(shí)現(xiàn)可視化埋點(diǎn)也有很多問(wèn)題需要解決,比如事件唯一標(biāo)識(shí)的確定娃殖,業(yè)務(wù)參數(shù)的獲取值戳,有邏輯判斷的埋點(diǎn)配置項(xiàng)信息等等。接下來(lái)我會(huì)重點(diǎn)圍繞唯一標(biāo)識(shí)以及業(yè)務(wù)參數(shù)獲取這兩個(gè)問(wèn)題給出自己的一個(gè)解決方案炉爆。
唯一標(biāo)識(shí)問(wèn)題
唯一標(biāo)識(shí)的組成方式主要是又 target + action 來(lái)確定堕虹, 即任何一個(gè)事件都存在一個(gè)target與action。 在此引入AOP編程芬首,AOP(Aspect-Oriented-Programming)即面向切面編程的思想鲫凶,基于 Runtime 的 Method Swizzling能力,來(lái) hook 相應(yīng)的方法衩辟,從而在hook方法中進(jìn)行統(tǒng)一的埋點(diǎn)處理螟炫。例如所有的按鈕被點(diǎn)擊時(shí),都會(huì)觸發(fā)UIApplication的sendAction方法艺晴,我們hook這個(gè)方法昼钻,即可攔截所有按鈕的點(diǎn)擊事件。
這里主要分為兩個(gè)部分 :
事件的鎖定
事件的鎖定主要是靠 “事件唯一標(biāo)識(shí)符”來(lái)鎖定封寞,而事件的唯一標(biāo)識(shí)是由我們寫(xiě)入配置表中的然评。埋點(diǎn)數(shù)據(jù)的上報(bào)。
埋點(diǎn)數(shù)據(jù)的數(shù)據(jù)又分為兩種類(lèi)型: 固定數(shù)據(jù)與可變的業(yè)務(wù)數(shù)據(jù)狈究, 而固定數(shù)據(jù)我們可以直接寫(xiě)到配置表中碗淌, 通過(guò)唯一標(biāo)識(shí)來(lái)獲取。而對(duì)于業(yè)務(wù)數(shù)據(jù),我是這么理解的: 數(shù)據(jù)是有持有者的亿眠, 例如我們Controller的一個(gè)屬性值碎罚, 又或者數(shù)據(jù)再M(fèi)odel的某一個(gè)層級(jí)。 這么的話我們就可以通過(guò)KVC的的方式來(lái)遞歸獲取該屬性的值來(lái)取到業(yè)務(wù)數(shù)據(jù)纳像, 代碼后面會(huì)有介紹荆烈。
整體代碼示例
由于iOS中的事件場(chǎng)景是多樣的, 在此我以UIControl, UITablview(collectionView與tableView基本相同)竟趾, UITapGesture憔购, UIViewController的PV統(tǒng)計(jì) 為例,介紹一下具體思路岔帽。
1玫鸟、UIViewController PV統(tǒng)計(jì)
頁(yè)面的統(tǒng)計(jì)較為簡(jiǎn)單,利用Method Swizzing hook 系統(tǒng)的viewDidLoad犀勒, 直接通過(guò)頁(yè)面名稱(chēng)即可鎖定頁(yè)面的展示代碼如下:
@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];
//從配置表中取參數(shù)的過(guò)程 1 固定參數(shù) 2 業(yè)務(wù)參數(shù)(此處參數(shù)被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 事件唯一標(biāo)識(shí)為:%@ \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", [self class], pageid, pagename, uploadDic);
}
}
2屎飘、UIControl 點(diǎn)擊統(tǒng)計(jì)
主要通過(guò)hook sendAction:to:forEvent: 來(lái)實(shí)現(xiàn), 其唯一標(biāo)識(shí)符我們用 targetname/selector/tag來(lái)標(biāo)記,具體代碼如下:
~~~
@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 唯一標(biāo)識(shí)符為 : %@, \n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", identifier, eventid, targetname, pageid, pagename, uploadDic);
}
}
~~~
3账蓉、TableView (CollectionView) 的點(diǎn)擊統(tǒng)計(jì)
tablview的唯一標(biāo)識(shí)枚碗, 我們使用 delegate.class/tableview.class/tableview.tag的組合來(lái)唯一鎖定。 主要是通過(guò)hook setDelegate 方法铸本, 在設(shè)置代理的時(shí)候再去交互 didSelect 方法來(lái)實(shí)現(xiàn)肮雨, 具體的原理是 具體代碼如下:
@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
// 初始化一個(gè)名字為 delegate.class/tableview.class/tableview.tag 的selector
SEL sel_ = NSSelectorFromString([NSString stringWithFormat:@”%@/%ld”, [self class], self.tag]);
// 將生成的selector的方法 加入的 delegate類(lèi)中, 并且該方法的實(shí)現(xiàn)(IMP)指向當(dāng)前類(lèi)user_tableView:didSelectRowAtIndexPath: 方法的實(shí)現(xiàn)
class_addMethod([delegate class],
sel_,
method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))),
nil);
//判斷是否有實(shí)現(xiàn)箱玷,沒(méi)有的話添加一個(gè)實(shí)現(xiàn)
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_];
}
//判斷頁(yè)面是否實(shí)現(xiàn)了某個(gè)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 被調(diào)用的時(shí)候, 實(shí)質(zhì)調(diào)用的是以下方法:
-(void)user_tableView:(UITableView )tableView didSelectRowAtIndexPath:(NSIndexPath )indexPath
{
//通過(guò)唯一標(biāo)識(shí)的規(guī)則锡足, 找到原來(lái)的方法 (即tableView:didSelectRowAtIndexPath: 方法)
SEL sel = NSSelectorFromString([NSString stringWithFormat:@”%@/%ld”, [tableView class], tableView.tag]);
if ([self respondsToSelector:sel]) {
//以下是對(duì)方法的調(diào)用以及傳參波丰,performSelector 方法底層實(shí)現(xiàn)與此相似
IMP imp = [self methodForSelector:sel];
void (func)(id, SEL,id,id) = (void )imp;
func(self, sel,tableView,indexPath);
}
//配置表中, 事件唯一標(biāo)識(shí)即為key舶得, 通過(guò)key 取value掰烟, 取到了就說(shuō)明該事件配置的有埋點(diǎn)上傳
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];
//通過(guò)containIn 參數(shù)判斷數(shù)據(jù)持有者,后續(xù)會(huì)有解釋
id instance = containIn == 0 ? self : cell;
id value = [CaptureTool captureVarforInstance:instance withPara:obj];
if (value && key) {
[uploadDic setObject:value forKey:key];
}
}];
NSLog(@"\n 事件的唯一標(biāo)識(shí)為 %@沐批, \n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", identifier, eventid, targetname, pageid, pagename, uploadDic);
}
}
4纫骑、gesture方式添加的的點(diǎn)擊統(tǒng)計(jì)
gesture的事件,是通過(guò) hook initWithTarget:action:方法來(lái)實(shí)現(xiàn)的九孩, 事件的唯一標(biāo)識(shí)依然是target.class/actionname來(lái)鎖定的先馆, 代碼如下:
@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事件的唯一標(biāo)識(shí)為 %@, \n event id === %@,\n target === %@, \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", identifier躺彬, eventid, targetname, pageid, pagename, uploadDic);
}
}
@end
配置表結(jié)構(gòu)
首先那煤墙, 配置表是一個(gè)json數(shù)據(jù)梅惯。 針對(duì)不同的場(chǎng)景 (UIControl , 頁(yè)面PV, Tabeview, Gesture)都做了區(qū)分仿野, 用不同的key區(qū)別铣减。 對(duì)于 “固定參數(shù)” , 我們之間寫(xiě)到配置表中设预,而對(duì)于業(yè)務(wù)參數(shù)徙歼, 我們之間寫(xiě)清楚參數(shù)在業(yè)務(wù)內(nèi)的名字犁河, 以及上傳時(shí)的 keyName鳖枕, 參數(shù)的持有者。 通過(guò)Runtime + KVC來(lái)取值桨螺。 配置表可以是這個(gè)樣子:(僅供參考)
{
"ACTION": {
"ViewController/jumpSecond": {
"userDefined": {
"eventid": "201803074|93",
"target": "",
"pageid": "234",
"pagename": "button點(diǎn)擊宾符,跳轉(zhuǎn)至下一個(gè)頁(yè)面"
},
"pagePara": {
"testKey9": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"SecondViewController/back": {
"userDefined": {
"eventid": "201803074|965",
"target": "second",
"pageid": "234",
"pagename": "button點(diǎn)擊,返回"
},
"pagePara": {
"testKey9": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
}
},
"PAGEPV": {
"ViewController": {
"userDefined": {
"pageid": "234",
"pagename": "XXX 頁(yè)面展示了"
},
"pagePara": {
"testKey10": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"SecondViewController": {
"userDefined": {
"pageid": "234",
"pagename": "XXX頁(yè)面展示"
},
"pagePara": {
"testKey0": {
"propertyName": "age",
"propertyPath":"",
"containIn": "0"
}
}
}
},
"TABLEVIEW": {
"ViewController/TestTableview/0":{
"userDefined": {
"eventid": "201803074|93",
"target": "",
"pageid": "234",
"pagename": "tableview 被點(diǎn)擊"
},
"pagePara": {
"user_grade": {
"propertyName": "grade",
"propertyPath":"",
"containIn": "1"
}
}
}
},
"GESTURE": {
"ViewController/gesture1clicked:":{
"userDefined": {
"eventid": "201803074|93",
"target": "",
"pageid": "手勢(shì)1對(duì)應(yīng)的id",
"pagename": "手勢(shì)1對(duì)應(yīng)的page name"
},
"pagePara": {
"testKey1": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"ViewController/gesture2clicked:":{
"userDefined": {
"eventid": "201803074|93",
"target": "",
"pageid": "手勢(shì)2對(duì)應(yīng)的id",
"pagename": "手勢(shì)2對(duì)應(yīng)的page name"
},
"pagePara": {
"testKey2": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"SecondViewController/gesture3clicked:":{
"userDefined": {
"eventid": "201803074|98",
"target": "",
"pageid": "gesture3clicked",
"pagename": "手勢(shì)3對(duì)應(yīng)的page name"
},
"pagePara": {
"user_age": {
"propertyName": "goodsnumber",
"propertyPath":"",
}
}
}
}
}
取參方法
#import "CaptureTool.h"
#import <objc/runtime.h>
@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(@"自定義的類(lèi)----- %@", className);
const char * name = property_getName(property);
NSString * varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding];
[varNameArray addObject:varname];
} else {
// NSLog(@"系統(tǒng)的類(lèi)");
}
}
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"];
// 實(shí)例中包含其他對(duì)象的情況
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