懶人做開(kāi)發(fā)系列:利用Object-C特性埋點(diǎn)

Objective-C是一門(mén)簡(jiǎn)單的語(yǔ)言,95%是C。只是在語(yǔ)言層面上加了些關(guān)鍵字和語(yǔ)法。真正讓Objective-C如此強(qiáng)大的是它的運(yùn)行時(shí)。它很小但卻很強(qiáng)大。它的核心是消息分發(fā)。
運(yùn)行時(shí)會(huì)發(fā)消息給對(duì)象。一個(gè)對(duì)象的class保存了方法列表。那么這些消息是如何映射到方法的,這些方法又是如何被執(zhí)行的呢?第一個(gè)問(wèn)題的答案很簡(jiǎn)單。class的方法列表其實(shí)是一個(gè)字典,key為selectors,IMPs為value。一個(gè)IMP是指向方法在內(nèi)存中的實(shí)現(xiàn)。很重要的一點(diǎn)是,selector和IMP之間的關(guān)系是在運(yùn)行時(shí)才決定的,而不是編譯時(shí)。這樣我們就能玩出些花樣。
這次我們就是利用運(yùn)行時(shí)來(lái)進(jìn)行配置化的埋點(diǎn)。首先說(shuō)下什么是埋點(diǎn):所謂埋點(diǎn)就是在應(yīng)用中特定的流程收集一些信息,用來(lái)跟蹤應(yīng)用使用的狀況,后續(xù)用來(lái)進(jìn)一步優(yōu)化產(chǎn)品或是提供運(yùn)營(yíng)的數(shù)據(jù)支撐,包括訪問(wèn)(Visits),訪客(Visitor),停留時(shí)間(Time On Site),頁(yè)面查看(Page Views,又稱(chēng)為頁(yè)面瀏覽)和跳出率(Bounce Rate,又可稱(chēng)為蹦失率)。這樣的信息收集可以大致分為兩種:頁(yè)面統(tǒng)計(jì)(track this virtual page view),統(tǒng)計(jì)操作行為(track this button by an event)。
這種的正常做法就是在各自的頁(yè)面的viewWillAppear以及按鈕的點(diǎn)擊實(shí)現(xiàn)里去加代碼傳輸數(shù)據(jù)給服務(wù)端進(jìn)行統(tǒng)計(jì),這種方式雖然省腦子,但是既耗時(shí)間,也不便于后期維護(hù)。
利用語(yǔ)言的特性我們對(duì)這種方式進(jìn)行改進(jìn),首先我們要用到Aspects框架,Aspects是iOS平臺(tái)一個(gè)輕量級(jí)的面向切面編程(AOP)框架,只包括兩個(gè)方法:一個(gè)類(lèi)方法,一個(gè)實(shí)例方法。核心原理就是:


1513759-4e30c9b337c4c891.png

下面我們來(lái)看下實(shí)現(xiàn):首先需要新建一個(gè)plist把你需要的埋點(diǎn)都加進(jìn)去:


image.png

然后看下代碼實(shí)現(xiàn):
- (void)trackEvent {
   // Hook viewcontroller
   NSString *filePath = [[NSBundle mainBundle] pathForResource:@"KZWList" ofType:@"plist"];
   NSDictionary *configs = [NSDictionary dictionaryWithContentsOfFile:filePath];
   
   [UIViewController aspect_hookSelector:@selector(viewWillAppear:)
                             withOptions:AspectPositionAfter
                              usingBlock:^(id<AspectInfo> aspectInfo) {
                                  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                      NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                      NSString *pageImp = configs[className][@"KZWTrackPageName"];
                                      if (pageImp) {
                                          id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
                                          [tracker set:kGAIScreenName value:pageImp];
                                          [tracker send:[[GAIDictionaryBuilder createScreenView] build]];
                                      }
                                  });
                              } error:NULL];

   // Hook Events
   for (NSString *className in configs) {
       Class clazz = NSClassFromString(className);
       NSDictionary *config = configs[className];
       NSString *pageImp = configs[className][@"KZWTrackPageName"];
       if (config[@"KZWTrackEvents"]) {
           for (NSDictionary *event in config[@"KZWTrackEvents"]) {
               SEL selekor = NSSelectorFromString(event[@"KZWEventSelector"]);

               [clazz aspect_hookSelector:selekor
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) {
                                   //將參數(shù)發(fā)到自己服務(wù)器
                                   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                   id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
                                   [tracker send:[[GAIDictionaryBuilder createEventWithCategory:pageImp
                                                                                         action:event[@"KZWEventAction"]
                                                                                          label:event[@"KZWEventName"]
                                                                                          value:nil] build]];
                                       });
                               } error:NULL];

           }
       }
   }
}

下面我們來(lái)說(shuō)說(shuō)該方案的缺陷:
1、并不是所有的事件都是有繼承自UIControl的控件來(lái)發(fā)出的,比如:手勢(shì),點(diǎn)擊Cell。
2、并不是所有的按鈕點(diǎn)擊了之后就立馬需要埋點(diǎn)上傳?可能在按鈕的響應(yīng)方法中經(jīng)過(guò)了層層的if(){ } else{ }最后才需要埋點(diǎn)。
3、如果有參數(shù)
4、對(duì)于代理方法該怎樣處理?
5、如果很多個(gè)按鈕對(duì)應(yīng)著一個(gè)事件該怎樣處理?
6、項(xiàng)目中事件的處理方法不盡相同,方法的參數(shù)個(gè)數(shù)不一樣,并且方法的返回值也不一樣,如何對(duì)他們進(jìn)行統(tǒng)一的處理?
下面我們來(lái)一一解決這些問(wèn)題。
問(wèn)題1:對(duì)于不是來(lái)自UIControl的子類(lèi)發(fā)出的事件,我們一樣是可以進(jìn)行hooK,只不過(guò)方法有所不同。我們?cè)赨IControl的分類(lèi)中寫(xiě)了一段嵌入的代碼,確實(shí)hook住了系統(tǒng)UIButton的點(diǎn)擊事件,是因?yàn)閁IButton自身會(huì)調(diào)用UIControl的這個(gè)方法。但是對(duì)于點(diǎn)擊事件,這個(gè)是我們自己寫(xiě)的一個(gè)方法,它的父類(lèi)UIViewController中是沒(méi)有的,所以在執(zhí)行我們自己點(diǎn)擊事件的方法時(shí)UIViewController分類(lèi)中要嵌入的方法是不會(huì)被調(diào)用的,這時(shí)候怎么辦,我們可以動(dòng)態(tài)的給我們自己要hook的ViewController動(dòng)態(tài)的添加一個(gè)方法,然后就可以hook了(這一點(diǎn)不太好理解)。具體的添加方法,可以參考本文的實(shí)例代碼。

問(wèn)題2:對(duì)于是否上傳和具體的業(yè)務(wù)邏輯相關(guān)的情況,我們可以用方法所在類(lèi)的一個(gè)屬性值進(jìn)行標(biāo)記,這個(gè)屬性寫(xiě)在.m文件中即可(KVC可以獲取.m文件中的屬性值。),我們先執(zhí)行要hook那個(gè)類(lèi)的方法,然后根據(jù)plist中配置的相關(guān)標(biāo)記進(jìn)行相應(yīng)的處理(這里的屬性值其實(shí)也是不必要的,我么可以根據(jù)類(lèi)名和方法名字符串的哈希生成唯一的key,然后利用runtime自動(dòng)關(guān)聯(lián)到這個(gè)類(lèi)的mf_condition屬性上,這個(gè)屬性是一個(gè)字典其key就是剛才生成的,value就是運(yùn)行完這個(gè)方法之后得到的值,然后這個(gè)值再跟plist中的配置做以比較)。

問(wèn)題3:對(duì)于和事件所在類(lèi)有緊密關(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ù)。

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

問(wèn)題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)處理。

問(wèn)題6:這個(gè)問(wèn)題其實(shí)就是hook住所有的方法,然后給他們添加同一個(gè)代碼段的問(wèn)題,這時(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的分類(lèi)中調(diào)用這個(gè)接口的對(duì)象不一樣,并且我們根據(jù)plist中的配置hook的selector不一樣,然而最后執(zhí)行的block卻是一樣的,這就很好的解決了問(wèn)題。
實(shí)在不好這樣埋的部分埋點(diǎn),可以選擇方法一進(jìn)行埋點(diǎn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容