懶人做開發系列:利用Object-C特性埋點

mooncoder發表於2018-04-23

Objective-C是一門簡單的語言,95%是C。只是在語言層面上加了些關鍵字和語法。真正讓Objective-C如此強大的是它的執行時。它很小但卻很強大。它的核心是訊息分發。 執行時會發訊息給物件。一個物件的class儲存了方法列表。那麼這些訊息是如何對映到方法的,這些方法又是如何被執行的呢?第一個問題的答案很簡單。class的方法列表其實是一個字典,key為selectors,IMPs為value。一個IMP是指向方法在記憶體中的實現。很重要的一點是,selector和IMP之間的關係是在執行時才決定的,而不是編譯時。這樣我們就能玩出些花樣。 這次我們就是利用執行時來進行配置化的埋點。首先說下什麼是埋點:所謂埋點就是在應用中特定的流程收集一些資訊,用來跟蹤應用使用的狀況,後續用來進一步優化產品或是提供運營的資料支撐,包括訪問(Visits),訪客(Visitor),停留時間(Time On Site),頁面檢視(Page Views,又稱為頁面瀏覽)和跳出率(Bounce Rate,又可稱為蹦失率)。這樣的資訊收集可以大致分為兩種:頁面統計(track this virtual page view),統計操作行為(track this button by an event)。 這種的正常做法就是在各自的頁面的viewWillAppear以及按鈕的點選實現裡去加程式碼傳輸資料給服務端進行統計,這種方式雖然省腦子,但是既耗時間,也不便於後期維護。 利用語言的特性我們對這種方式進行改進,首先我們要用到Aspects框架,Aspects是iOS平臺一個輕量級的面向切面程式設計(AOP)框架,只包括兩個方法:一個類方法,一個例項方法。核心原理就是:

1513759-4e30c9b337c4c891.png
下面我們來看下實現:首先需要新建一個plist把你需要的埋點都加進去:
image.png
然後看下程式碼實現:

- (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) {
                                   //將引數發到自己伺服器
                                   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];

           }
       }
   }
}
複製程式碼

下面我們來說說該方案的缺陷:

  1. 並不是所有的事件都是有繼承自UIControl的控制元件來發出的,比如:手勢,點選Cell。
  2. 並不是所有的按鈕點選了之後就立馬需要埋點上傳?可能在按鈕的響應方法中經過了層層的if(){ } else{ }最後才需要埋點。
  3. 如果有引數
  4. 對於代理方法該怎樣處理?
  5. 如果很多個按鈕對應著一個事件該怎樣處理?
  6. 專案中事件的處理方法不盡相同,方法的引數個數不一樣,並且方法的返回值也不一樣,如何對他們進行統一的處理? 下面我們來一一解決這些問題。 問題1:對於不是來自UIControl的子類發出的事件,我們一樣是可以進行hooK,只不過方法有所不同。我們在UIControl的分類中寫了一段嵌入的程式碼,確實hook住了系統UIButton的點選事件,是因為UIButton自身會呼叫UIControl的這個方法。但是對於點選事件,這個是我們自己寫的一個方法,它的父類UIViewController中是沒有的,所以在執行我們自己點選事件的方法時UIViewController分類中要嵌入的方法是不會被呼叫的,這時候怎麼辦,我們可以動態的給我們自己要hook的ViewController動態的新增一個方法,然後就可以hook了(這一點不太好理解)。具體的新增方法,可以參考本文的例項程式碼。

問題2:對於是否上傳和具體的業務邏輯相關的情況,我們可以用方法所在類的一個屬性值進行標記,這個屬性寫在.m檔案中即可(KVC可以獲取.m檔案中的屬性值。),我們先執行要hook那個類的方法,然後根據plist中配置的相關標記進行相應的處理(這裡的屬性值其實也是不必要的,我麼可以根據類名和方法名字串的雜湊生成唯一的key,然後利用runtime自動關聯到這個類的mf_condition屬性上,這個屬性是一個字典其key就是剛才生成的,value就是執行完這個方法之後得到的值,然後這個值再跟plist中的配置做以比較)。

問題3:對於和事件所在類有緊密關聯的埋點資料,比如某個頁面對應的產品ID,比如某個頁面點選了cell,之後這個cell對應的model的ID。這個時候我們可以參考方法2,新增一個屬性,用一個屬性值來儲存這些這些需要上傳的具體資料。

問題4:代理方法和手勢的處理也是一樣的,既然一個類實現了某個代理方法,那麼其[someInstance respondsToSelector:someSelector]所返回的BOOL值應該是YES的,然後其它的就和手勢的處理是一樣的了。

問題5:對於很多按鈕對應一個響應事件的情況,我們可以利用RunTime動態的給按鈕新增一個屬性,比如:buttonIdentifier,這樣我們就可以在plist中進行相應的配置,以進行相應的埋點處理。

問題6:這個問題其實就是hook住所有的方法,然後給他們新增同一個程式碼段的問題,這時候我們可以使用Aspects這個第三方框架:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                  withOptions:(AspectOptions)options
                   usingBlock:(id)block
                        error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
 }
複製程式碼

呼叫這個介面,因為在UIViewController的分類中呼叫這個介面的物件不一樣,並且我們根據plist中的配置hook的selector不一樣,然而最後執行的block卻是一樣的,這就很好的解決了問題。 實在不好這樣埋的部分埋點,可以選擇方法一進行埋點。

相關文章