標籤:instance 響應 遇到的問題 進入 turn 配置資訊 post fonts 操作
??打造一款符合自己公司需求的使用者行為統計系統,相信是非常多運營人員的夢想,也是開發人員對技術的的執著追求。
以下我為大家分一享下自己為公司打造的使用者行為統計系統。
??使用者行為統計(User Behavior Statistics, UBS)一直是移動互連網產品中不可缺少的環節,也俗稱埋點。對於產品經理,運營人員來說。埋點當然是越多,覆蓋範圍越廣越好。廢話廢話就不多少了,這裡我主要利用了AOP面向切片編程的思想來解決問題的。參考部落格:參考部落格地址?首先聲明,我這裡並沒有全然照搬別人部落格。這裡主要是順著別人部落格思路去走,走進死胡同,然後返璞歸真,用自己的思路去實現的。
之所以把別人的思路寫下來討論。就是為了說明思考的過程有時也非常重要。
使用者行為統計統計什嗎?
??我們經常說使用者行為統計,那麼使用者行為統計主要統什計麼呢,在我看來主要分為兩類:1,頁面統計:PV ;2,事件統計:Event。
頁面統計:PV
??頁面統計就是就在使用者進入某個頁面的時候。進記行錄儲存。在使用者離開某個頁面的時候進行儲存記錄。
在當適的時候將儲存的資料發送給後台server。實現代碼例如以下:
[UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){ [self JKhandlePV:data status:JKUBSPV_ENTER]; } error:nil]; [UIViewController aspect_hookSelector:@selector(viewDidDisappear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){ [self JKhandlePV:data status:JKUBSPV_LEAVE]; } error:nil];
非常多部落格貼出這種代碼以為就攻克了問題。事實上忽略了非常大的一個問題,這樣簡粗單暴的去處理,會發現項中目所的有UIViewCnotroller的這兩個方法viewDidAppear:
,viewDidDisappear:
都被會hook,造了成額外的效能開銷,非常的不好。
所以我邊這進行了處理僅僅針對要統的計頁面進行hook操作。具現體執行個體如以下:
+ (void)configPV{ for (NSString *vcName in [[JKUBS shareInstance].configureData[JKUBSPVKey] allKeys]) { Class target = NSClassFromString(vcName); [target aspect_hookSelector:@selector(viewDidAppear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){ [self JKhandlePV:data status:JKUBSPV_ENTER]; } error:nil]; [target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){ [self JKhandlePV:data status:JKUBSPV_LEAVE]; } error:nil]; }}
事件統計:Event
??事件統計主要是在使用者觸發事件時進行記錄儲存,然後在合適的時候將記的錄資料發送給後台server進行處理。
依照文章開頭參考部落格所說,簡單將件事分成了UIButotn,UIControl,UIGestureRecognizer以及點擊UITableView儲存格cell觸發的事件。點擊UICollectionView儲存格cell觸發的事件。
??依照這個思路我首先對UIButton,UIControl觸發的事件進行處理:
+ (void)configUIControlEvent{ [UIControl aspect_hookSelector:@selector(sendAction:to:forEvent:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id<JKUBSAspectInfo> data){ [self JKHandleEvent:data]; } error:nil];}
這個實現起來相對easy些,相信大家都有實現過。
??對UIGestureRecognizer
觸發的事件進行處理,比較麻煩 首先UIGestureRecognizer
是一個類簇,我們觸發事件時的tap,LongPress,swipe,pan等手勢發送事件是並非發送事件的真正的類。我這邊通過打斷點的形式找到了發送事件的真正的類是:UIGestureRecognizerTarget
發送事件的私人方法是:_sendActionWithGestureRecognizer:
然後我就通過hook操作對手勢觸發的事件進行了處理:
+ (void)configGestureRecognizerEvent{ Class UIGestureRecognizerTarget =NSClassFromString(@"UIGestureRecognizerTarget"); [UIGestureRecognizerTarget aspect_hookSelector:@selector(_sendActionWithGestureRecognizer:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id<JKUBSAspectInfo> data){ [self JKHandleEvent:data]; } error:nil];}
對手勢觸發的事件進行統計儘管困難,但還是實現了。
??對於點擊UITableView儲存格cell觸發的事件,點擊UICollectionView儲存格cell觸發的事件。我這邊以點擊UITableView儲存格cell觸發的事件為例進行說明。假設JKBViewController
實現了UITableView
的代理方法tableView:didSelectRowAtIndexPath:
那麼我的實現例如以下:
+ (void)configureDelegateEvent{ [JKBViewController aspect_hookSelector:@selector(tableView:didSelectRowAtIndexPath:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id<JKUBSAspectInfo> data){ [self JKHandleEvent:data]; } error:nil];}
通過這個實現我們能夠做到對點擊UITableView儲存格cell觸發的事件進行統計,可是順著參博考客作者的思路這一步一步做下來。做到這裡我內心有種不的妙感覺。
走進死胡同
以下是參考的部落格作者在開發的過程中遇到的問題
1。並非全部的事件都是有繼承自UIControl的空間來發出的。比方:手勢。點擊Cell。2。並非全部的button點擊了之後就立刻須要埋點上傳?可能在button的回應程式法中經過了層層的if(){ } else{ }最後才須要埋點。
4,對於代理方法該如何處理?5,假設非常多個button相應著一個事件該如何處理?
其針實對第1點,我邊這儘管梳理了非常多類型的事件。可是仍然有非常多沒有被統計上,比方搖一搖觸發的事件。計步器觸發的事件。tabBar點擊觸發的事件等,還非常有多我可能沒到想的事件。我現發假設依照作者的意圖,依照事件觸發的類型去一個一個的進行hook操作的話,工作兩蠻大。並且還是會有遺漏的。尤是其涉及到有方些法蘋果沒有開放給開發人員,我們進行處理的話比較麻煩。
開員發人估被計要累死啊。
針對第2點,按作照者的意圖,會現發點擊之后里面還有層層的推斷。如何繞過層層的推斷呢?這個我會在接下來詳細闡述。
針對第4點。我在上面已經實現過了。
針對第5點,在現實的情況中確實存在者不同的頁面中。甚至同樣的頁面中不同的button相應著同一個事件這種問題。
假設依照參考部落格作者的思路確實處理起來非常是麻煩。
返璞歸真
??針對上面出現的困境。我在想有沒有更好的辦法去解決呢。
首先想到我們統計使用者操的作事件,並是不為了統計使用者點擊了某個button,或者進行了某個手勢操作。調了用某個代理方法。而為是了統計使用者進行這個操作的目的是什麼。是為了購物。還是為了分享等。所以我就打破參考部落格作者的思路,不再對button,手勢。儲存格選中等事件進行hook。而是對使用者的目的事件觸發的方法進行hook,事件就是事件,沒有來源之分。也就是hook就提示的事件。中介層層的邏輯推斷,我不須要考慮。我僅僅考慮hook的目的事件。
舉例個子,使用者要行進分享- (void)goShare;
,我不關心用是戶否點擊了button,或者tap手勢觸發了方法,或者儲存格被中選。我僅僅關心分享的方法- (void)goShare;
有沒有被調用。被調用的時候我能否夠進記行錄操作。
另外唯一確定一個方法。除了selector,還要有相關的target(方法的實現者,或者訊息接受者)。針上面第5點,不同button相應同一個事件,普通情況下事件同樣target不同,我們是能夠差別的出來的。
當瞭然也存在同一個頁面上的不同button觸發的同一個事件,這種情況下不是太常見,函數外麵包一層。改個別的名字區分一下就好了。只是EnvetID還是要一樣的。
??為了更好的方便大家。我這邊按自照己的思路寫了一個pod庫。以下先說一下自己的plist檔案檔案:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFuaGFpbG9uZzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="這裡寫圖片描寫敘述" title="">
大家能夠看到PV欄位下,每個頁面都以可設定頁面的名字,還一有些其它的資訊。
Event欄位下有EventID,同一時候呢也同意同一個EventID下有不同的觸發事件。
事件1這一級欄位寫上詳細的事件內容,主要是方便開發人讀員閱尋找。
JKVC1點擊,JKVC2點擊,tap單擊。選中tableView儲存格這些都是為了標件來明事源,方便開發人員閱讀。另外假設事件還須要配置額外的參數。那麼能夠在EventID同級欄位下加入新的內容。
下看看面來代碼吧:
JKUBS.h
#import <Foundation/Foundation.h>#import "JKUBSAspects.h"extern NSString const *JKUBSPVKey;extern NSString const *JKUBSEventKey;extern NSString const *JKUBSEventIDKey;extern NSString const *JKUBSEventConfigKey;extern NSString const *JKUBSSelectorStrKey;extern NSString const *JKUBSTargetKey;typedef NS_ENUM(NSInteger, JKUBSPVSTATUS){ JKUBSPV_ENTER = 0, //進入頁面 JKUBSPV_LEAVE //離開頁面};@interface JKUBS : NSObject@property (nonatomic,strong,readonly) NSDictionary *configureData;/** 產生單例的方法 @return 單例對象 */+ (instancetype)shareInstance;/** 通過json設定檔匯入配置資訊json設定檔或plist設定檔僅僅匯入一個就好了 @param jsonFilePath json檔案沙箱路徑 */+ (void)configureDataWithJSONFile:(NSString *)jsonFilePath;/** 通過plist設定檔匯入配置資訊json設定檔或plist設定檔僅僅匯入一個就好了 @param plistFileName plist檔案名稱字(不帶尾碼名) */+ (void)configureDataWithPlistFile:(NSString *)plistFileName;/** 處理PV這種方法須要開發人員重載進行詳細的操作 @param data 頁面資訊 @param status 進入或離開頁面的狀態 */+ (void)JKhandlePV:(id<JKUBSAspectInfo>)data status:(JKUBSPVSTATUS)status;/** 處理事件這種方法須要開發人員重載進行詳細的操作 @param data 事件資訊 @param eventId 事件ID */+ (void)JKHandleEvent:(id<JKUBSAspectInfo>)data EventID:(NSInteger)eventId;@end
JKUBS.m
#import "JKUBS.h"NSString const *JKUBSPVKey = @"PV";NSString const *JKUBSEventKey = @"Event";NSString const *JKUBSEventIDKey = @"EventID";NSString const *JKUBSEventConfigKey = @"EventConfig";NSString const *JKUBSSelectorStrKey = @"selectorStr";NSString const *JKUBSTargetKey = @"target";@interface JKUBS()@property (nonatomic,strong,readwrite) NSDictionary *configureData;@end@implementation JKUBSstatic JKUBS *_ubs =nil;+ (instancetype)shareInstance{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _ubs = [JKUBS new]; }); return _ubs;}+ (void)configureDataWithJSONFile:(NSString *)jsonFilePath{ NSData *data = [NSData dataWithContentsOfFile:jsonFilePath]; NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; [JKUBS shareInstance].configureData = dic; if ([JKUBS shareInstance].configureData) { [self setUp]; }}+ (void)configureDataWithPlistFile:(NSString *)plistFileName{ NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:plistFileName ofType:@"plist"]]; [JKUBS shareInstance].configureData = dic; if ([JKUBS shareInstance].configureData) { [self setUp]; }}+ (void)setUp{ [self configPV]; [self configEvents];}#pragma mark PVConfig - - - -+ (void)configPV{ for (NSString *vcName in [[JKUBS shareInstance].configureData[JKUBSPVKey] allKeys]) { Class target = NSClassFromString(vcName); [target aspect_hookSelector:@selector(viewDidAppear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){ [self JKhandlePV:data status:JKUBSPV_ENTER]; } error:nil]; [target aspect_hookSelector:@selector(viewDidDisappear:) withOptions:JKUBSAspectPositionAfter usingBlock:^(id data){ [self JKhandlePV:data status:JKUBSPV_LEAVE]; } error:nil]; }}+ (void)JKhandlePV:(id<JKUBSAspectInfo>)data status:(JKUBSPVSTATUS)status{}#pragma mark EventConfig - - - -+ (void)configEvents{ NSDictionary *eventsDic = [JKUBS shareInstance].configureData[JKUBSEventKey]; NSArray *events =[eventsDic allValues]; for (NSDictionary *dic in events) { NSInteger EventID = [dic[JKUBSEventIDKey] integerValue]; NSArray *eventConfigs = [dic[JKUBSEventConfigKey] allValues]; for (NSDictionary *eventConfig in eventConfigs) { NSString *selectorStr = eventConfig[JKUBSSelectorStrKey]; NSString *targetClass = eventConfig[JKUBSTargetKey]; Class target =NSClassFromString(targetClass); SEL selector = NSSelectorFromString(selectorStr); [target aspect_hookSelector:selector withOptions:JKUBSAspectPositionBefore usingBlock:^(id<JKUBSAspectInfo> data){ [self JKHandleEvent:data EventID:EventID]; } error:nil]; } }}+ (void)JKHandleEvent:(id<JKUBSAspectInfo>)data EventID:(NSInteger)eventId{}
當中有兩個方法要重點說一下。
+ (void)JKhandlePV:(id<JKUBSAspectInfo>)data status:(JKUBSPVSTATUS)status。+ (void)JKHandleEvent:(id<JKUBSAspectInfo>)data EventID:(NSInteger)eventId;
這兩個方法都須要在JKUBS的category進行重載,來做詳細的實現。
比如頁面活動的記錄,事件的記錄。打造使用者行為統計系統。我這邊已經完畢了AOP思想下的事件採集。詳細如何記錄,儲存,發給送後台,這裡就不詳細說明了。
代碼
使用pod例如以下:
pod "JKUBS"
注意:demo中我對aspects庫進行了改動。為了防止名字衝突,我這邊統一都加了JKUBS首碼。
歡迎大家來找茬,一塊交流學習。
iOS打造屬於自己的使用者行為統計系統