RunTime應(yīng)用實(shí)例--關(guān)于埋點(diǎn)的思考

親兄猩,我的簡書已不再維護(hù)和更新了,所有文章都遷移到了我的個(gè)人博客:https://mikefighting.github.io/峻仇,歡迎交流弓颈。

埋點(diǎn)是現(xiàn)在很多App中都需要用到的,這個(gè)問題可能每個(gè)人都能處理涩维,但是怎樣來減少埋點(diǎn)所帶來的侵入性殃姓,怎樣用更加簡潔的方式來處理埋點(diǎn)問題,怎樣減少誤埋瓦阐,如果上線了發(fā)現(xiàn)少埋了怎么辦蜗侈?下面是本文討論的重點(diǎn)(本文Demo已上傳GitHub,可以下載討論):

一睡蟋、什么是埋點(diǎn)踏幻?埋點(diǎn)的作用是什么?
二戳杀、常規(guī)的處理方式是怎樣的该面?
三、我們可以怎樣優(yōu)化信卡?
四隔缀、怎樣使用RunTime對其進(jìn)行優(yōu)化?
五傍菇、在實(shí)踐中遇到了什么問題以及解決方案猾瘸?
六、最理想的埋點(diǎn)是什么樣的?
七牵触、其中可能存在的問題是什么淮悼?

接下來將對其一一做以說明:

一、什么是埋點(diǎn)荒吏?埋點(diǎn)的作用是什么敛惊?

其實(shí)埋點(diǎn)也叫日志上報(bào),其實(shí)就是根據(jù)需求上報(bào)一系列關(guān)于用戶行為的數(shù)據(jù)绰更,比如:用戶點(diǎn)擊了哪個(gè)按鈕瞧挤,用戶瀏覽了哪個(gè)網(wǎng)站,用戶在某個(gè)頁面停留了多久等數(shù)據(jù)儡湾。這些數(shù)據(jù)對于運(yùn)營來說很有用特恬,他們可以用來分析某個(gè)功能開發(fā)的是不是合理,是不是因?yàn)槟硞€(gè)地方的不合理而到導(dǎo)致了轉(zhuǎn)化率的下降徐钠,從而對我們的App進(jìn)行相應(yīng)的改進(jìn)癌刽,我們來看下某個(gè)第三方平臺提供的埋點(diǎn)實(shí)例。


埋點(diǎn)統(tǒng)計(jì)字段定義

上圖中說明了尝丐,某個(gè)時(shí)間對應(yīng)的事件ID,以及針對這個(gè)事件需要關(guān)聯(lián)的字段显拜。下面是后臺系統(tǒng)對某個(gè)埋點(diǎn)所做的數(shù)據(jù)統(tǒng)計(jì):
[圖片上傳失敗...(image-5a9faa-1512050850766)]


后臺系統(tǒng)對埋點(diǎn)的數(shù)據(jù)分析

這樣我們就可以詳細(xì)的分析出用戶對于App的反饋,從而及時(shí)的修改我們的產(chǎn)品爹袁。

二远荠、常規(guī)的埋點(diǎn)的處理方式是怎樣的?

其實(shí)很簡單,我們就在相應(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è)一下按鈕就請求一次網(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ì)造成難以測試校辩?5.這樣的頻繁上報(bào)會(huì)不會(huì)增加服務(wù)器端的壓力?6.代碼整潔嗎辆童?......(程序員的一個(gè)好習(xí)慣是:這個(gè)代碼能否經(jīng)受住需求的變更。)

三惠赫、我們可以怎樣優(yōu)化把鉴?

  1. 首先我們可以用一個(gè)類,來專門處理這些需要上報(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í)間
  1. 對于用戶id庭砍,當(dāng)前時(shí)間场晶,用戶手機(jī)型號,手機(jī)品牌怠缸,等等與用戶所在頁面無關(guān)的內(nèi)容诗轻,可以用統(tǒng)一的一個(gè)類進(jìn)行處理,將其作為這個(gè)類的一個(gè)屬性揭北,使用getter方法將其相應(yīng)的數(shù)值返回即可(對于恒定不變的可以使用懶加載)扳炬。
  2. 這樣的數(shù)據(jù)傳輸策略是有問題的,每次點(diǎn)擊都上報(bào)搔体,可能一個(gè)面需要上報(bào)的地方很多恨樟,這就會(huì)造成很大的性能問題,我們可以先將需要上傳的數(shù)據(jù)緩存起來疚俱,然后緩存夠50條數(shù)據(jù)上報(bào)一次劝术,或者每隔5分鐘上報(bào)一次;
  3. 為了節(jié)省流量我們可以,1)將數(shù)據(jù)壓縮之后再上報(bào),可以參考我的另一篇文章呆奕;2)和服務(wù)端商量养晋,用盡可能短的字段,如:cityName = @"北京";變?yōu)?code>cn = @"北京";3)盡量不要上傳的頻率過高梁钾,如第三點(diǎn)绳泉。
  4. 如何解決代碼的整潔,易于測試的問題陈轿?請看下面圈纺。

四、怎樣使用RunTime來進(jìn)行優(yōu)化麦射?

我么能不能利用RunTime來給每一個(gè)Button的響應(yīng)事件中添加一段代碼蛾娶,利用這段代碼來進(jìn)行埋點(diǎn)上報(bào)呢?或者進(jìn)一步來說我們能不能給所有繼承自UIControl的對象都添加這樣一段代碼呢潜秋?這樣我們不是可以捕獲所有的用戶事件了嗎蛔琅?(其實(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)了色徘,那么我們怎樣從這么多的埋點(diǎn)中選出這個(gè)這個(gè)埋點(diǎn)呢?我們其實(shí)可以用字典和數(shù)組結(jié)合的方式將這些方法的target和方法的參數(shù)一一存起來操禀,然后在嵌入的方法內(nèi)部獲取其對應(yīng)的方法褂策,以及其相應(yīng)的,這個(gè)事先配置好的字典和數(shù)組的結(jié)合放在哪里比較合適呢颓屑?plist斤寂。下面就以最簡單的形式展示這種思路:

    // 工具類
    @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)用的是原來的系統(tǒng)方法
[self sendSwizzleAction:action to:target forEvent:event];

NSString *selectorName = NSStringFromSelector(action);

// 這個(gè)plist中存儲的數(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è)方法對應(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í)踐中遇到了什么問題以及解決方案扬蕊?

  1. 并不是所有的事件都是有繼承自UIControl的控件來發(fā)出的,比如:手勢丹擎,點(diǎn)擊Cell尾抑。
  2. 并不是所有的按鈕點(diǎn)擊了之后就立馬需要埋點(diǎn)上傳?可能在按鈕的響應(yīng)方法中經(jīng)過了層層的if(){ } else{ }最后才需要埋點(diǎn)蒂培。
  3. 和事件所在類無關(guān)的埋點(diǎn)數(shù)據(jù)可以同意從ZHLogDataCenter這個(gè)類中中取再愈,那么如果這個(gè)數(shù)據(jù)是和所在類有關(guān)呢?
  4. 對于代理方法該怎樣處理护戳?
  5. 如果很多個(gè)按鈕對應(yīng)著一個(gè)事件該怎樣處理翎冲?
  6. 項(xiàng)目中事件的處理方法不盡相同,方法的參數(shù)個(gè)數(shù)不一樣媳荒,并且方法的返回值也不一樣抗悍,如何對他們進(jìn)行統(tǒng)一的處理?
    下面我們來一一解決這些問題。

問題1:對于不是來自UIControl的子類發(fā)出的事件钳枕,我們一樣是可以進(jìn)行hooK缴渊,只不過方法有所不同。我們在UIControl的分類中寫了一段嵌入的代碼鱼炒,確實(shí)hook住了系統(tǒng)UIButton的點(diǎn)擊事件衔沼,是因?yàn)閁IButton自身會(huì)調(diào)用UIControl的這個(gè)方法。但是對于點(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:對于是否上傳和具體的業(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)的處理(這里的屬性值其實(shí)也是不必要的,我么可以根據(jù)類名和方法名字符串的哈希生成唯一的key凿傅,然后利用runtime自動(dòng)關(guān)聯(lián)到這個(gè)類的mf_condition屬性上缠犀,這個(gè)屬性是一個(gè)字典其key就是剛才生成的,value就是運(yùn)行完這個(gè)方法之后得到的值聪舒,然后這個(gè)值再跟plist中的配置做以比較)辨液。

問題3:對于和事件所在類有緊密關(guān)聯(lián)的埋點(diǎn)數(shù)據(jù),比如某個(gè)頁面對應(yīng)的產(chǎn)品ID,比如某個(gè)頁面點(diǎn)擊了cell箱残,之后這個(gè)cell對應(yīng)的model的ID滔迈。這個(gè)時(shí)候我們可以參考方法2,添加一個(gè)屬性被辑,用一個(gè)屬性值來存儲這些這些需要上傳的具體數(shù)據(jù)燎悍。

問題4:代理方法和手勢的處理也是一樣的,既然一個(gè)類實(shí)現(xiàn)了某個(gè)代理方法盼理,那么其[someInstance respondsToSelector:someSelector]所返回的BOOL值應(yīng)該是YES的谈山,然后其它的就和手勢的處理是一樣的了。

問題5:對于很多按鈕對應(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)樵?code>UIViewController的分類中調(diào)用這個(gè)接口的對象不一樣潜叛,并且我們根據(jù)plist中的配置hook的selector不一樣,然而最后執(zhí)行的block卻是一樣的壶硅,這就很好的解決了問題威兜。

六、最理想的埋點(diǎn)是什么樣的庐椒?

最理想的埋點(diǎn)是動(dòng)態(tài)的椒舵,就是PM給我們說需要哪些埋點(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)行拓哺,那么也將造難以處理勇垛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市士鸥,隨后出現(xiàn)的幾起案子闲孤,更是在濱河造成了極大的恐慌,老刑警劉巖烤礁,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讼积,死亡現(xiàn)場離奇詭異,居然都是意外死亡脚仔,警方通過查閱死者的電腦和手機(jī)勤众,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鲤脏,“玉大人们颜,你說我怎么就攤上這事×源迹” “怎么了窥突?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長硫嘶。 經(jīng)常有香客問我阻问,道長,這世上最難降的妖魔是什么沦疾? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任称近,我火速辦了婚禮第队,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刨秆。我一直安慰自己凳谦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布坛善。 她就那樣靜靜地躺著晾蜘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪眠屎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天肆饶,我揣著相機(jī)與錄音改衩,去河邊找鬼。 笑死驯镊,一個(gè)胖子當(dāng)著我的面吹牛葫督,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播板惑,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼橄镜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冯乘?” 一聲冷哼從身側(cè)響起洽胶,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎裆馒,沒想到半個(gè)月后姊氓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喷好,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年翔横,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梗搅。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡禾唁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出无切,到底是詐尸還是另有隱情荡短,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站禀酱,受9級特大地震影響谨娜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烫映,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一沼本、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锭沟,春花似錦抽兆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祝辣,卻和暖如春贴妻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蝙斜。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工名惩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人孕荠。 一個(gè)月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓娩鹉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稚伍。 傳聞我的和親對象是個(gè)殘疾皇子弯予,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容