iOS打造屬於自己的用戶行為統計系統
??打造一款符合自己公司需求的用戶行為統計系統,相信是非常多運營人員的夢想,也是開發人員對技術的的執著追求。
以下我為大家分一享下自己為公司打造的用戶行為統計系統。
??用戶行為統計(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文件文件:
大家能夠看到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 JKUBS
static 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打造屬於自己的用戶行為統計系統