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)框架,只包括兩個方法:一個類方法,一個實例方法。核心原理就是: 編程
- (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
問題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