埋點(diǎn)是現(xiàn)在很多App中都需要用到的,這個(gè)問題可能每個(gè)人都能處理,但是怎樣來(lái)減少埋點(diǎn)所帶來(lái)的侵入性,怎樣用更加簡(jiǎn)潔的方式來(lái)處理埋點(diǎn)問題,怎樣減少誤埋,如果上線了發(fā)現(xiàn)少埋了怎么辦?下面是本文討論的重點(diǎn):
一、什么是埋點(diǎn)?埋點(diǎn)的作用是什么?
二、常規(guī)的處理方式是怎樣的?
三、我們可以怎樣優(yōu)化?
四、怎樣使用RunTime對(duì)其進(jìn)行優(yōu)化?
五、在實(shí)踐中遇到了什么問題以及解決方案?
六、最理想的埋點(diǎn)是什么樣的?
七、其中可能存在的問題是什么?
接下來(lái)將對(duì)其一一做以說(shuō)明:
一、什么是埋點(diǎn)?埋點(diǎn)的作用是什么?
其實(shí)埋點(diǎn)也叫日志上報(bào),其實(shí)就是根據(jù)需求上報(bào)一系列關(guān)于用戶行為的數(shù)據(jù),比如:用戶點(diǎn)擊了哪個(gè)按鈕,用戶瀏覽了哪個(gè)網(wǎng)站,用戶在某個(gè)頁(yè)面停留了多久等數(shù)據(jù)。這些數(shù)據(jù)對(duì)于運(yùn)營(yíng)來(lái)說(shuō)很有用,他們可以用來(lái)分析某個(gè)功能開發(fā)的是不是合理,是不是因?yàn)槟硞€(gè)地方的不合理而到導(dǎo)致了轉(zhuǎn)化率的下降,從而對(duì)我們的App進(jìn)行相應(yīng)的改進(jìn),我們來(lái)看下某個(gè)第三方平臺(tái)提供的埋點(diǎn)實(shí)例。
埋點(diǎn)統(tǒng)計(jì)字段定義
上圖中說(shuō)明了,某個(gè)時(shí)間對(duì)應(yīng)的事件ID,以及針對(duì)這個(gè)事件需要關(guān)聯(lián)的字段。下面是后臺(tái)系統(tǒng)對(duì)某個(gè)埋點(diǎn)所做的數(shù)據(jù)統(tǒng)計(jì):
后臺(tái)系統(tǒng)對(duì)埋點(diǎn)的數(shù)據(jù)分析
這樣我們就可以詳細(xì)的分析出用戶對(duì)于App的反饋,從而及時(shí)的修改我們的產(chǎn)品。
二、常規(guī)的埋點(diǎn)的處理方式是怎樣的?
其實(shí)很簡(jiǎn)單,我們就在相應(yīng)的事件里面加入相關(guān)的代碼,給服務(wù)器上報(bào)數(shù)據(jù)不就得了。如下所示:
// 這個(gè)一個(gè)按鈕的響應(yīng)事件
- (void)someButtonAction:(UIButton *)someButton{
// 該按鈕需要處理的業(yè)務(wù)
[self upDateSomthing]
// 開始埋點(diǎn)
// eid:事件id,sa:用戶id, cI:當(dāng)前時(shí)間
NSDictionary *upLoadDic = @{@"eid":@"311",@"sa":@"706976487532177",@"cI":@"2016-6-4 12:11:34"};
[ZHUpLoadManager upLoadWithDic:upLoadDic];
}
這樣一個(gè)埋點(diǎn)問題就解決了,單同時(shí)卻隱藏著很多問題:1.這樣每點(diǎn)擊一個(gè)一下按鈕就請(qǐng)求一次網(wǎng)絡(luò)會(huì)不會(huì)出現(xiàn)性能問題?2.如果這樣頻繁的數(shù)據(jù)上報(bào)會(huì)不會(huì)消耗更多的用戶流量?3.這樣的代碼能經(jīng)受住需求的變更嗎?比如字段變了,或者你把cI看錯(cuò)了,應(yīng)該是cl。4.這樣的代碼會(huì)不會(huì)造成難以測(cè)試?5.這樣的頻繁上報(bào)會(huì)不會(huì)增加服務(wù)器端的壓力?6.代碼整潔嗎?……(程序員的一個(gè)好習(xí)慣是:這個(gè)代碼能否經(jīng)受住需求的變更。)
三、我們可以怎樣優(yōu)化?
1.首先我們可以用一個(gè)類,來(lái)專門處理這些需要上報(bào)的埋點(diǎn)的字段,將這些字段作為常量,例如:
// LogManager.h
extern NSString * const kLogEventKey; //事件id
extern NSString * const kLogUserIdKey; //用戶id
extern NSString * const kLogOperationInterval; //操作時(shí)間
// LogManager.m
NSString * const kLogEventKey = @"co"; //事件id
NSString * const kLogUserIdKey = @"sa"; //用戶id
NSString * const kLogOperationInterval = @"cq"; //操作時(shí)間
2.對(duì)于用戶id,當(dāng)前時(shí)間,用戶手機(jī)型號(hào),手機(jī)品牌,等等與用戶所在頁(yè)面無(wú)關(guān)的內(nèi)容,可以用統(tǒng)一的一個(gè)類進(jìn)行處理,將其作為這個(gè)類的一個(gè)屬性,使用getter方法將其相應(yīng)的數(shù)值返回即可(對(duì)于恒定不變的可以使用懶加載)。
3.這樣的數(shù)據(jù)傳輸策略是有問題的,每次點(diǎn)擊都上報(bào),可能一個(gè)面需要上報(bào)的地方很多,這就會(huì)造成很大的性能問題,我們可以先將需要上傳的數(shù)據(jù)緩存起來(lái),然后緩存夠50條數(shù)據(jù)上報(bào)一次,或者每隔5分鐘上報(bào)一次;
4.為了節(jié)省流量我們可以,1)將數(shù)據(jù)壓縮之后再上報(bào),可以參考我的另一篇文章;2)和服務(wù)端商量,用盡可能短的字段,如:cityName = @"北京";變?yōu)閏n = @"北京";3)盡量不要上傳的頻率過高,如第三點(diǎn)。
5.如何解決代碼的整潔,易于測(cè)試的問題?請(qǐng)看下面。
四、怎樣使用RunTime來(lái)進(jìn)行優(yōu)化?
我么能不能利用RunTime來(lái)給每一個(gè)Button的響應(yīng)事件中添加一段代碼,利用這段代碼來(lái)進(jìn)行埋點(diǎn)上報(bào)呢?或者進(jìn)一步來(lái)說(shuō)我們能不能給所有繼承自UIControl的對(duì)象都添加這樣一段代碼呢?這樣我們不是可以捕獲所有的用戶事件了嗎?(其實(shí)答案是否定的,看第五條);這時(shí)我們可以利用Mehod Swizzle,或者叫方法注入,或者叫hook住了某個(gè)方法,聽著挺玄乎,其實(shí)就是RunTime的一個(gè)API,這個(gè)API能夠交換兩個(gè)方法的實(shí)現(xiàn)。通過這個(gè)API,我們可以這樣實(shí)現(xiàn)方法注入。如下圖所示:
方法注入的實(shí)現(xiàn)過程
那么我們點(diǎn)擊按鈕系統(tǒng)會(huì)不會(huì)給每個(gè)按鈕都執(zhí)行一個(gè)統(tǒng)一的方法?然后我們往這個(gè)方法中嵌入響應(yīng)的代碼片段就可以了。答案是肯定的。我們可以往
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
這個(gè)方法里面嵌入相應(yīng)的代碼片段。我們可以這樣:1.將互換方法實(shí)現(xiàn)的的這個(gè)方法放到一個(gè)工具類中,因?yàn)槲覀兛赡懿恢挂惶幰玫竭@種方法。2.我們給UIControl添加一個(gè)Category,然后在里面調(diào)用這個(gè)工具類然后實(shí)現(xiàn)所插入的代碼片段。這里我們既然可以得到target還有action,那么很多情況下我們就可以唯一確定這個(gè)埋點(diǎn)了,那么我們?cè)鯓訌倪@么多的埋點(diǎn)中選出這個(gè)這個(gè)埋點(diǎn)呢?我們其實(shí)可以用字典和數(shù)組結(jié)合的方式將這些方法的target和方法的參數(shù)一一存起來(lái),然后在嵌入的方法內(nèi)部獲取其對(duì)應(yīng)的方法,以及其相應(yīng)的,這個(gè)事先配置好的字典和數(shù)組的結(jié)合放在哪里比較合適呢?plist。下面就以最簡(jiǎn)單的形式展示這種思路:
// 工具類
@interface ZHSwizzleTool : NSObject
+ (void)zhSwizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector;
@end
@implementation ZHSwizzleTool
+(void)zhSwizzleWithClass:(Class)processedClass originalSelector:(SEL)originSelector swizzleSelector:(SEL)swizzlSelector{
Method originMethod = class_getInstanceMethod(processedClass, originSelector);
Method swizzleMethod = class_getInstanceMethod(processedClass, swizzlSelector);
BOOL didAddMethod = class_addMethod(processedClass, originSelector, method_getImplementation(swizzleMethod),method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(processedClass, swizzlSelector, method_getImplementation(originMethod),method_getTypeEncoding(originMethod));
}else{
method_exchangeImplementations(originMethod, swizzleMethod);
}
}
@end
// 分類
@implementation UIControl (ZHSwizzle)
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originSEL = @selector(sendAction:to:forEvent:);
SEL swizzleSEL = @selector(sendSwizzleAction:to:forEvent:);
[ZHSwizzleTool zhSwizzleWithClass:[self class]originalSelector:originSEL swizzleSelector:swizzleSEL];
});
}
- (void)sendSwizzleAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
// 注意這里調(diào)用的是原來(lái)的系統(tǒng)方法
[self sendSwizzleAction:action to:target forEvent:event];
NSString *selectorName = NSStringFromSelector(action);
// 這個(gè)plist中存儲(chǔ)的數(shù)據(jù)格式是這樣的:@{@"someViewController":@"selector0":@[para0,para1,para2],@"selector1":@[para0,para1]]};
NSString *pathString = [[NSBundle mainBundle]pathForResource:@"ZHLogInfo" ofType:@"plist"];
NSDictionary *plistDic = [NSDictionary dictionaryWithContentsOfFile:pathString];
//1. 獲取Target的名字
NSDictionary *controllerDic = plistDic[NSStringFromClass([target class])];
//2. 獲取這個(gè)方法對(duì)應(yīng)的參數(shù)列表
NSArray *parameterArray = controllerDic[selectorName];
//3. 實(shí)例化數(shù)據(jù)中心
ZHLogDataCenter *logCenter = [[ZHLogDataCenter alloc]init];
NSMutableDictionary *logInfoDic = [NSMutableDictionary dictionary];
for (NSString *parameter in parameterArray) {
NSString *getSelector = [NSString stringWithFormat:@"%@",parameter];
SEL getSeletor = NSSelectorFromString(getSelector);
//4. 從數(shù)據(jù)中心中獲取相應(yīng)的數(shù)據(jù)
id value = [logCenter performSelector:getSeletor withObject:nil];
//5.獲取成功則將其存入需要上傳的字典
if (value)
[logInfoDic setObject:value forKey:parameter];
}
//6.將這個(gè)字典存入埋點(diǎn)管理類,其會(huì)將其存入緩存并等待上傳
[ZHLogCenter zhLogWithInforDictionary:logInfoDic];
}
@end
下面是這個(gè)代碼中用到的Plist中的配置:
埋點(diǎn)相關(guān)字段的plist配置
五、在實(shí)踐中遇到了什么問題以及解決方案?
并不是所有的事件都是有繼承自UIControl的空間來(lái)發(fā)出的,比如:手勢(shì),點(diǎn)擊Cell。
并不是所有的按鈕點(diǎn)擊了之后就立馬需要埋點(diǎn)上傳?可能在按鈕的響應(yīng)方法中經(jīng)過了層層的if(){ } else{ }最后才需要埋點(diǎn)。
和事件所在類無(wú)關(guān)的埋點(diǎn)數(shù)據(jù)可以同意從ZHLogDataCenter這個(gè)類中中取,那么如果這個(gè)數(shù)據(jù)是和所在類有關(guān)呢?
對(duì)于代理方法該怎樣處理?
如果很多個(gè)按鈕對(duì)應(yīng)著一個(gè)事件該怎樣處理?
項(xiàng)目中事件的處理方法不盡相同,方法的參數(shù)個(gè)數(shù)不一樣,并且方法的返回值也不一樣,如何對(duì)他們進(jìn)行統(tǒng)一的處理?
下面我們來(lái)一一解決這些問題。
問題1:對(duì)于不是來(lái)自UIControl的子類發(fā)出的事件,我們一樣是可以進(jìn)行hooK,只不過方法有所不同。我們?cè)赨IControl的分類中寫了一段嵌入的代碼,確實(shí)hook住了系統(tǒng)UIButton的點(diǎn)擊事件,是因?yàn)閁IButton自身會(huì)調(diào)用UIControl的這個(gè)方法。但是對(duì)于點(diǎn)擊事件,這個(gè)是我們自己寫的一個(gè)方法,它的父類UIViewController中是沒有的,所以在執(zhí)行我們自己點(diǎn)擊事件的方法時(shí)UIViewController分類中要嵌入的方法是不會(huì)被調(diào)用的,這時(shí)候怎么辦,我們可以動(dòng)態(tài)的給我們自己要hook的ViewController動(dòng)態(tài)的添加一個(gè)方法,然后就可以hook了(這一點(diǎn)不太好理解)。具體的添加方法,可以參考本文的實(shí)例代碼。
問題2:對(duì)于是否上傳和具體的業(yè)務(wù)邏輯相關(guān)的情況,我們可以用方法所在類的一個(gè)屬性值進(jìn)行標(biāo)記,這個(gè)屬性寫在.m文件中即可(KVC可以獲取.m文件中的屬性值。),我們先執(zhí)行要hook那個(gè)類的方法,然后根據(jù)plist中配置的相關(guān)標(biāo)記進(jìn)行響應(yīng)的處理。
問題3:對(duì)于和事件所在類有緊密關(guān)聯(lián)的埋點(diǎn)數(shù)據(jù),比如某個(gè)頁(yè)面對(duì)應(yīng)的產(chǎn)品ID,比如某個(gè)頁(yè)面點(diǎn)擊了cell,之后這個(gè)cell對(duì)應(yīng)的model的ID。這個(gè)時(shí)候我們可以參考方法2,添加一個(gè)屬性,用一個(gè)屬性值來(lái)存儲(chǔ)這些這些需要上傳的具體數(shù)據(jù)。
問題4:代理方法和手勢(shì)的處理也是一樣的,既然一個(gè)類實(shí)現(xiàn)了某個(gè)代理方法,那么其[someInstance respondsToSelector:someSelector]所返回的BOOL值應(yīng)該是YES的,然后其它的就和手勢(shì)的處理是一樣的了。
問題5:對(duì)于很多按鈕對(duì)應(yīng)一個(gè)響應(yīng)事件的情況,我們可以利用RunTime動(dòng)態(tài)的給按鈕添加一個(gè)屬性,比如:buttonIdentifier,這樣我們就可以在plist中進(jìn)行相應(yīng)的配置,以進(jìn)行相應(yīng)的埋點(diǎn)處理。
問題6:這個(gè)問題其實(shí)就是hook住所有的方法,然后給他們添加同一個(gè)代碼段的問題,這時(shí)候我們可以使用Aspects這個(gè)第三方框架:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}
調(diào)用這個(gè)接口,因?yàn)樵赨IViewController的分類中調(diào)用這個(gè)接口的對(duì)象不一樣,并且我們根據(jù)plist中的配置hook的selector不一樣,然而最后執(zhí)行的block卻是一樣的,這就很好的解決了問題。
六、最理想的埋點(diǎn)是什么樣的?
最理想的埋點(diǎn)是動(dòng)態(tài)的,就是PM給我們說(shuō)需要哪些埋點(diǎn),然后服務(wù)器給我們發(fā)一個(gè)類似與上文中提到的plist一樣的文件,或者一個(gè)json,我們存到本地,如果這些埋點(diǎn)沒有更新,我們就從本地中讀取相應(yīng)的文件,做相應(yīng)的埋點(diǎn),如果有更新,我們重新從服務(wù)器獲取最新的需要埋的點(diǎn),然后進(jìn)行相應(yīng)埋點(diǎn)。這樣就解決了少埋,或者埋點(diǎn)不恰當(dāng),需要添加埋點(diǎn)的問題。
七、其中可能存在的問題是什么?
當(dāng)然這里面也有其難以處理的問題,比如我們使用了一個(gè)第三方控件,這個(gè)第三方控件的事件回調(diào)不是用delegate實(shí)現(xiàn)的,而是用block實(shí)現(xiàn)的,并且這個(gè)埋點(diǎn)和具體的業(yè)務(wù)邏輯有關(guān)系,那么這種方法就難以處理了。 如果很多事件的邏輯處理放到了block中進(jìn)行,那么也將造難以處理。