用戶統(tǒng)計實現(xiàn)

IOS用戶統(tǒng)計實現(xiàn)

用戶統(tǒng)計

用戶行為統(tǒng)計(User Behavior Statistics, UBS)一直是移動互聯(lián)網(wǎng)產(chǎn)品中必不可少的環(huán)節(jié)姊舵,也俗稱埋點。在保證移動端流量不會受較大影響的前提下彼硫,PM們總是希望埋點覆蓋面越廣越好。目前常規(guī)的做法是將埋點代碼封裝成工具類凌箕,但凡工程中需要埋點(如點擊事件拧篮、頁面跳轉(zhuǎn))的地方都插入埋點代碼。一旦項目越來越復(fù)雜牵舱,你會發(fā)現(xiàn)埋點的代碼散落在程序的各個角落串绩,不利于維護(hù)以及復(fù)用。

常規(guī)埋點做法

接著開頭的話題仆葡,我們先回顧一下主流的埋點是怎么做的。我粗糙地將埋點分為兩種:
1、頁面統(tǒng)計沿盅,包括頁面停留時間把篓、頁面進(jìn)入次數(shù);
2腰涧、交互事件統(tǒng)計韧掩,包括單擊、雙擊窖铡、手勢交互等疗锐。

常規(guī)頁面統(tǒng)計埋點

以統(tǒng)計頁面進(jìn)入次數(shù)為例,最簡單粗暴的做法是在所有頁面的viewDidAppear:以及viewDidDisappear:中分別埋點费彼,將自己對應(yīng)的pageID上傳給服務(wù)端滑臊。代碼大概長醬紫:

@implementation HomeViewController
- (void)viewDidAppear:(BOOL)animated{

    [super viewWillAppear:animated];
    [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];

}

- (void)viewDidDisappear:(BOOL)animated{

    [super viewDidDisappear:animated];[WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];

}
@end

+[WUserStatistics sendEventToServer:]封裝網(wǎng)絡(luò)請求,將ID上傳給服務(wù)器箍铲。上述方案有以下弊端:
1.復(fù)用性差雇卷。這部分埋點代碼很難給其他項目復(fù)用。
2.工作量大颠猴。尤其當(dāng)頁面較多時关划,需要修改的代碼較多。
3.引入“臟代碼”翘瓮,不易維護(hù)贮折。

第3點提到的“臟代碼”意思是用戶行為分析這種業(yè)務(wù)其實跟主業(yè)務(wù)沒太大關(guān)系,不應(yīng)該保持如此高的耦合度资盅,因為這些代碼會干擾我們對項目主業(yè)務(wù)的維護(hù)调榄。

常規(guī)交互事件埋點

常規(guī)做法一般在交互事件的selector中獲取該事件的ID并上傳給服務(wù)端,代碼大概長醬紫:

- (IBAction)onFavBtnPressed:(id)sender{    
    [WUserStatistics  sendEventToServer:@"CTRL_EVENT_HOME_FAV"];         
//...do other things  }

稍微大一點的APP如果采用這種方式律姨,那諸如此類的埋點代碼將遍地都是振峻。它的缺點參考頁面統(tǒng)計埋點部分,其復(fù)用性基本為零择份,也就是在新項目中根本無法復(fù)用埋點代碼扣孟。 小總結(jié)一下,采用常規(guī)的做法雖然直觀方便荣赶,但在可復(fù)用性凤价、可維護(hù)性等方面有所欠缺。在我看來拔创,借助運(yùn)行時可以很好地避開這些缺點利诺。

Method Swizzling、Hook與代碼注入

在iOS中剩燥,我們可以在運(yùn)行時替換兩個方法的實現(xiàn)慢逾,達(dá)到“勾住”某個方法并注入代碼的目的立倍。具體做法是: 重載類的“+(void)load”方法,在程序加載到內(nèi)存時利用Runtime的method_exchangeImplementations等接口將方法(設(shè)為M)的實現(xiàn)互相交換侣滩。當(dāng)方法M被調(diào)用時就會被勾住(Hook)口注,執(zhí)行我們的方法。 這種技術(shù)也稱為Method Swizzling君珠,屬于面向切面編程(Aspect-Oriented Programming)的一種實現(xiàn)寝志。 替換兩個方法的實現(xiàn),代碼一般長醬紫:

    @interface WHookUtility : NSObject
    + (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
    @end

    @implementation WHookUtility
    + (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{  
    
        Class class = cls;
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class, originalSelector,
        method_getImplementation(swizzledMethod),
        method_getTypeEncoding(swizzledMethod));

        if(didAddMethod) {
    
        class_replaceMethod(class,
        swizzledSelector,
        method_getImplementation(originalMethod),
        method_getTypeEncoding(originalMethod));
   
       }else{
        method_exchangeImplementations(originalMethod,   swizzledMethod);
        }
     }  
@end  

這個WHookUtility工具類下文會用到策添。比如現(xiàn)在我們要勾住UIViewController的viewWillAppear:方法材部,可以這樣做:

@implementation UIViewController (userStastistics)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{  

    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(swiz_viewWillAppear:);
    [WHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];  

  });
}

- (void)swiz_viewWillAppear:(BOOL)animated{
    //插入需要執(zhí)行的代碼
    NSLog(@"我在viewWillAppear執(zhí)行前偷偷插入了一段代碼");
    [self swiz_viewWillAppear:animated];
}
@end

基于運(yùn)行時的埋點方案

為了便于下文敘述,先引入一個簡單的項目唯竹,共有兩個頁面(HomeViewController乐导,DetailViewController) 需求是:

  • 統(tǒng)計兩個頁面的展示與離開次數(shù)
  • 統(tǒng)計收藏、分享單擊事件的次數(shù)
  • 對現(xiàn)有工程代碼影響越小越好

統(tǒng)計兩個頁面的展示與離開次數(shù)

這部分應(yīng)該比較直觀了摩窃,摒棄掉在每個controller中埋點的方式兽叮,我們對UIViewController添加category從而Hook到viewWillAppear:與viewWillDisappear:。在這兩個方法中注入埋點代碼:

@implementation UIViewController (userStastistics)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{  

    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(swiz_viewWillAppear:);
    [WHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];  

    SEL originalSelector2 = @selector(viewWillDisappear:);
    SEL swizzledSelector2 = @selector(swiz_viewWillDisappear:);
    [WHookUtility swizzlingInClass:[self class] originalSelector:originalSelector2        swizzledSelector:swizzledSelector2];
  });

}  

pragma mark - Method Swizzling

- (void)swiz_viewWillAppear:(BOOL)animated{

     //插入需要執(zhí)行的代碼
     [self inject_viewWillAppear];
     [self swiz_viewWillAppear:animated];

  }  
  
 - (void)swiz_viewWillDisappear:(BOOL)animated{

     [self inject_viewWillDisappear];
     [self swiz_viewWillDisappear:animated];

 }  
      
  //利用hook 統(tǒng)計所有頁面的停留時長

  - (void)inject_viewWillAppear{

     NSString *pageID = [self pageEventID:YES];
     if (pageID) {
     [WUserStatistics sendEventToServer:pageID];
       }
  }

 - (void)inject_viewWillDisappear{

     NSString *pageID = [self pageEventID:NO];
     if (pageID) {
     [WUserStatistics sendEventToServer:pageID];
     }
  }

這時候問題來了猾愿,項目中每個頁面都會有自己的頁面事件編號(pageEventID)鹦聪,此處的埋點代碼如何知道要發(fā)送什么pageEventID給服務(wù)端呢?輕松祭出if-else神器: 為了便于下文敘述蒂秘,先引入一個簡單的項目泽本,共有兩個頁面(HomeViewController,DetailViewController)姻僧,如下:

- (NSString *)pageEventID:(BOOL)bEnterPage{

    NSString *selfClassName = NSStringFromClass([self class]);
    NSString *pageEventID = nil;  

    if ([selfClassName isEqualToString:@"HomeViewController"]) {  

    pageEventID = bEnterPage ? @"EVENT_HOME_ENTER_PAGE" : @"EVENT_HOME_LEAVE_PAGE"; 
     }else if([selfClassName isEqualToString:@"DetailViewController"]) {  
  
    pageEventID = bEnterPage ? @"EVENT_DETAIL_ENTER_PAGE" : @"EVENT_DETAIL_LEAVE_PAGE";
  }

    //else if (<#expression#>)...
}

當(dāng)然规丽,我們可以有更優(yōu)雅的方式,比如用一個配置表替代上面一長串的if判斷撇贺,這樣無論頁面數(shù)怎么增加赌莺,代碼始終是那么一小段。我們新建一個WGlobalUserStatisticsConfig.plist的配置表來存放每個頁面在進(jìn)入以及離開時的pageEventID松嘶,結(jié)構(gòu)如下:

PageEventID.png
  • (上效果圖可見用戶統(tǒng)計實現(xiàn)圖片文件夾中的PageEventID.png)

因此艘狭,頁面進(jìn)出統(tǒng)計中獲取pageEventID的代碼始終是以下這幾句:

- (NSString *)pageEventID:(BOOL)bEnterPage{

    NSDictionary *configDict = [self dictionaryFromUserStatisticsConfigPlist];
    NSString *selfClassName = NSStringFromClass([self class]);
    return configDict[selfClassName][@"PageEventIDs"][bEnterPage ? @"Enter" : @"Leave"];
}

- (NSDictionary *)dictionaryFromUserStatisticsConfigPlist{

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"WGlobalUserStatisticsConfig" ofType:@"plist"];
    NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:filePath];

    return dic;
}

效果如下:

Jietu.png

  • (上效果圖可見用戶統(tǒng)計實現(xiàn)圖片文件夾中的Jietu.png)

以上就是完成了頁面進(jìn)出統(tǒng)計的埋點,并且達(dá)到了我們的第三點預(yù)期:對現(xiàn)有代碼基本無影響翠订。通過Method Swizzling的方式現(xiàn)有的工程甚至不需要import任何文件巢音!后期代碼變動時需要維護(hù)的僅僅是plist配置表。

統(tǒng)計收藏尽超、分享單擊事件的次數(shù)

與上一節(jié)思路一致官撼,要做到解耦顯然需要通過category+hook來實現(xiàn)。本文demo中收藏跟分享都是UIButton類型似谁,可以考慮添加UIButton的catogory傲绣。但更好的方式是添加UIControl的category掠哥,這樣可以讓埋點代碼覆蓋到所有UIControl的子類中去,比如button秃诵、switch龙致、segment等,提高復(fù)用性顷链。 既然要hook,那就要清楚到底要hookUIControl的哪(幾)個方法屈梁,只有部分方法是滿足埋點需求的嗤练,最好是所hook的方法能提供target、actionName等信息在讶。這是個嘗試的過程煞抬。代碼如下:

@implementation UIControl (userStastistics)

+(void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    SEL originalSelector =  @selector(sendAction:to:forEvent:);
    SEL swizzledSelector = @selector(swiz_sendAction:to:forEvent:);
    [WHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];
});
}

pragma mark - Method Swizzling

- (void)swiz_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{

    //插入埋點代碼
    [self performUserStastisticsAction:action to:target forEvent:event];
    [self swiz_sendAction:action to:target forEvent:event];
}
- (void)performUserStastisticsAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld", NSStringFromSelector(action), target , (long)event);
}
@end

Log如下圖:

Log.png

  • (上效果圖可見用戶統(tǒng)計實現(xiàn)圖片文件夾中的Log.png)

可以看到,通過category+method swizzling的方式在沒有修改現(xiàn)有工程任何代碼的情況下已經(jīng)成功Hook到所有點擊事件构哺,在Hook代碼中我們知道了一個點擊事件的target也就是ViewController革答,也知道了點擊事件的響應(yīng)函數(shù)名,知道了點擊的TouchSet曙强。這些信息已經(jīng)能滿足埋點需求了残拐。 與頁面統(tǒng)計埋點類似,我們同樣采用plist配置表的方式避免一大長串的if-else判斷: 有了這張配置表就很容易得到某次單擊事件的事件ID(ControlEventID):

NSString *actionString = NSStringFromSelector(action);//獲取SEL string
NSString *targetName = NSStringFromClass([target class]);//viewController name
NSDictionary *configDict = [self dictionaryFromUserStatisticsConfigPlist];
eventID = configDict[targetName][@"ControlEventIDs"][actionString];

事實上碟嘴,我把某個頁面單元的所有事件ID分成了兩類:頁面事件ID(PageEventIDs溪食,頁面的進(jìn)出等)、交互事件ID(ControlEventIDs娜扇,單擊错沃、雙擊、手勢等)雀瓢。分類有助于下文使用單元測試(Unit Test)進(jìn)行自動化后期維護(hù)枢析。 到這里先做了階段性的總結(jié),本文提出的思路有以下優(yōu)越性:

  • 與工程代碼基本解耦刃麸,避免引入“臟代碼”
  • 即使后期工程代碼發(fā)生重構(gòu)醒叁,需要修改的僅僅是plist配置表
  • 維護(hù)配置表比維護(hù)散落在工程各個角落的代碼簡單

小總結(jié):
合理的單元測試可以為本文方案的后期維護(hù)減輕相當(dāng)大的負(fù)擔(dān),測試用例的完備性很重要嫌蚤,需要用心設(shè)計考慮周全辐益。

結(jié)語

以上就是結(jié)合運(yùn)行時所設(shè)計出的用戶統(tǒng)計思路全部內(nèi)容。應(yīng)該說該方案的可復(fù)用性與解耦程度都是不錯的脱吱,既適合于新建的工程智政,也適合于已經(jīng)創(chuàng)建的工程∠潋穑看起來內(nèi)容多续捂,其實總結(jié)起來無非幾個步驟:plist配置表+Hook+單元測試垦垂。利用Method Swizzling把埋點代碼集中管理其實也是合理的,有利于專人開發(fā)牙瓢、跟蹤及維護(hù)劫拗。當(dāng)然以上思路只考慮簡單的情形,更復(fù)雜的情況就需要變通了矾克,但總體思路就是如此页慷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胁附,隨后出現(xiàn)的幾起案子酒繁,更是在濱河造成了極大的恐慌,老刑警劉巖控妻,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件州袒,死亡現(xiàn)場離奇詭異,居然都是意外死亡弓候,警方通過查閱死者的電腦和手機(jī)郎哭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來菇存,“玉大人夸研,你說我怎么就攤上這事∫琅福” “怎么了陈惰?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長毕籽。 經(jīng)常有香客問我抬闯,道長,這世上最難降的妖魔是什么关筒? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任溶握,我火速辦了婚禮,結(jié)果婚禮上蒸播,老公的妹妹穿的比我還像新娘睡榆。我一直安慰自己,他們只是感情好袍榆,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布胀屿。 她就那樣靜靜地躺著,像睡著了一般包雀。 火紅的嫁衣襯著肌膚如雪宿崭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天才写,我揣著相機(jī)與錄音葡兑,去河邊找鬼奖蔓。 笑死,一個胖子當(dāng)著我的面吹牛讹堤,可吹牛的內(nèi)容都是我干的吆鹤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洲守,長吁一口氣:“原來是場噩夢啊……” “哼疑务!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起梗醇,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤暑始,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后婴削,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡牙肝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年唉俗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片配椭。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡虫溜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出股缸,到底是詐尸還是另有隱情衡楞,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布敦姻,位于F島的核電站瘾境,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏镰惦。R本人自食惡果不足惜迷守,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旺入。 院中可真熱鬧兑凿,春花似錦、人聲如沸茵瘾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拗秘。三九已至圣絮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間雕旨,已是汗流浹背晨雳。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工行瑞, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人餐禁。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓血久,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帮非。 傳聞我的和親對象是個殘疾皇子氧吐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 聲明:本文是本人 編程小翁 原創(chuàng),轉(zhuǎn)載請注明末盔。 注:本文需要一些iOS的Runtime基礎(chǔ) 該方案的完成將會用到以...
    編程小翁閱讀 24,286評論 119 329
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉筑舅,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評論 0 9
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,099評論 25 707
  • 那人敲了兩下,停了下來陨舱。過了一會兒才接著敲了一下翠拣。 請不要敲了。時羽快步走上前游盲,制止了那個還想接著敲鈴的人误墓。師傅今...
    溫其言閱讀 160評論 -1 1
  • 世界上只有父母親對孩子關(guān)心, 如果你不愛你的父母親益缎,那么請深愛 還有一種現(xiàn)象父母養(yǎng)育一個孩子十幾年谜慌,那個孩...
    嘿小萌妹閱讀 197評論 2 2