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)框架,只包括兩個方法:一個類方法,一個例項方法。核心原理就是:
下面我們來看下實現:首先需要新建一個plist把你需要的埋點都加進去: 然後看下程式碼實現:- (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];
}
}
}
}
複製程式碼
下面我們來說說該方案的缺陷:
- 並不是所有的事件都是有繼承自UIControl的控制元件來發出的,比如:手勢,點選Cell。
- 並不是所有的按鈕點選了之後就立馬需要埋點上傳?可能在按鈕的響應方法中經過了層層的if(){ } else{ }最後才需要埋點。
- 如果有引數
- 對於代理方法該怎樣處理?
- 如果很多個按鈕對應著一個事件該怎樣處理?
- 專案中事件的處理方法不盡相同,方法的引數個數不一樣,並且方法的返回值也不一樣,如何對他們進行統一的處理? 下面我們來一一解決這些問題。 問題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卻是一樣的,這就很好的解決了問題。 實在不好這樣埋的部分埋點,可以選擇方法一進行埋點。