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

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];

           }
       }
   }
}
複製代碼

下面咱們來講說該方案的缺陷:bash

  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的,而後其它的就和手勢的處理是同樣的了。async

問題5:對於不少按鈕對應一個響應事件的狀況,咱們能夠利用RunTime動態的給按鈕添加一個屬性,好比:buttonIdentifier,這樣咱們就能夠在plist中進行相應的配置,以進行相應的埋點處理。優化

問題6:這個問題其實就是hook住全部的方法,而後給他們添加同一個代碼段的問題,這時候咱們可使用Aspects這個第三方框架:ui

+ (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倒是同樣的,這就很好的解決了問題。 實在很差這樣埋的部分埋點,能夠選擇方法一進行埋點。this

相關文章
相關標籤/搜索