1. 程式人生 > >iOS打造屬於自己的用戶行為統計系統

iOS打造屬於自己的用戶行為統計系統

不可 全部 pop cto objective ont ole nts markdown

??打造一款符合自己公司需求的用戶行為統計系統,相信是非常多運營人員的夢想,也是開發人員對技術的的執著追求。

以下我為大家分一享下自己為公司打造的用戶行為統計系統。


??用戶行為統計(User Behavior Statistics, UBS)一直是移動互聯網產品中不可缺少的環節,也俗稱埋點。對於產品經理,運營人員來說。埋點當然是越多,覆蓋範圍越廣越好。廢話廢話就不多少了,這裏我主要利用了AOP面向切片編程的思想來解決問題的。參考博客:參考博客地址?首先聲明,我這裏並沒有全然照搬別人博客。這裏主要是順著別人博客思路去走,走進死胡同,然後返璞歸真,用自己的思路去實現的。

之所以把別人的思路寫下來討論。就是為了說明思考的過程有時也非常重要。

用戶行為統計統計什麽?

??我們經常說用戶行為統計,那麽用戶行為統計主要統什計麽呢,在我看來主要分為兩類:1,頁面統計:PV2,事件統計: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打造屬於自己的用戶行為統計系統