iOS 埋點

所謂埋點就是App端采集用戶某些事件的數(shù)據(jù)铃慷,提交到服務器進行分析考抄。那某些事件是指哪些事件呢?譬如App的啟動和退出沐绒、瀏覽某個頁面俩莽、一些的點擊事件等等。項目中需要埋點一般用全埋點洒沦,何為全埋點豹绪?即是指無需應用程序開發(fā)工程師寫代碼或者只寫少量的代碼,即可預先自動收集用戶的所有或者絕大部分的行為數(shù)據(jù),然后再根據(jù)實際的業(yè)務分析需求從中篩選出所需行為數(shù)據(jù)并進行分析瞒津。下面是我項目中做的幾種事件埋點蝉衣。

一、App的啟動和退出

直接在AppDelegate實現(xiàn)對應的方法

回調(diào)方法 本地通知 通知時機
- applicationDidBecomeActive: UIApplicationDidBecomeActiveNotification 從后臺已經(jīng)進入前臺
- applicationDidEnterBackground: UIApplicationDidEnterBackgroundNotification 已經(jīng)進入后臺
- application:didFinishLaunchingWithOptions: UIApplicationDidFinishLaunchingNotification 進入程序
- applicationWillTerminate: UIApplicationWillTerminateNotification 將退出程序
AppDelegate.m
- (void)applicationWillTerminate:(UIApplication *)application
{
    // 將要退出程序巷蚪,調(diào)用對應接口
}

可以創(chuàng)建一個埋點管理類BuryPointManager病毡,用來提交數(shù)據(jù)給服務器

AppDelegate.m
- (void)applicationWillTerminate:(UIApplication *)application
{
    // 將要退出程序,調(diào)用對應接口
    [[BuryPointManager sharedInstance] 方法];
}

如果有SceneDelegate屁柏,applicationDidBecomeActive:啦膜、applicationDidEnterBackground:等方法在AppDelegate實現(xiàn)不了(application:didFinishLaunchingWithOptions:applicationWillTerminate:還是在AppDelegate實現(xiàn))淌喻,要去SceneDelegate實現(xiàn)僧家,applicationDidBecomeActive:applicationDidEnterBackground:對應SceneDelegate的sceneDidBecomeActive:裸删、sceneDidEnterBackground:

SceneDelegate.m
- (void)sceneDidBecomeActive:(UIScene *)scene {
     [[BuryPointManager sharedInstance] 方法];
}

二八拱、頁面瀏覽事件

頁面瀏覽事件分瀏覽某個頁面和統(tǒng)計某個頁面的瀏覽時長

方法一:用視圖控制器基類埋點

一般項目創(chuàng)建都會創(chuàng)建一個UIViewController的子類當頁面的基類,如叫BaseViewController涯塔,那么可以到這個類里面進行埋點

  • 瀏覽某個頁面
BaseViewController.m
- (void)viewDidLoad{
    [super viewDidLoad];

    // 如果是ViewController這個控制器就提交服務器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    }
}
  • 統(tǒng)計時長
BaseViewController.m

// 不能用viewWillAppear和viewDidDisappear或者viewDidAppear和viewWillDisappear統(tǒng)計肌稻,如viewWillAppear和viewDidDisappear,因為下一個頁面的viewWillAppear會比上一個頁面的viewDidDisappear觸發(fā)的快匕荸,因此[BuryPointManager sharedInstance].time會重置
- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear: animated];
    // 可以在管理類里面定義個屬性記錄當前時間
    [BuryPointManager sharedInstance].time = CFAbsoluteTimeGetCurrent();
}

- (void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear: animated];

    // 根據(jù)頁面出現(xiàn)和消失計算瀏覽時間
    NSTimeInterval time = CFAbsoluteTimeGetCurrent() -  [BuryPointManager sharedInstance].time;
    // 這里可以根據(jù)是哪個控制器再提交服務器, 方法里面帶上時間和需要計算時長的頁面控制器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    } else if () {

    } ...
    // 如果每次判斷麻煩爹谭,可以在BuryPointManager里面定義一個viewControllers的數(shù)組來存需要埋點的控制器
    for (NSString *name in [BuryPointManager sharedInstance].viewControllers) {
        if ([name isEqualToString:NSStringFromClass([self class])]) {
            [[BuryPointManager sharedInstance] 方法];
            break;
        }
    }

}

方法二:使用hook

首先先寫一個寫個分類實現(xiàn)方法替換

NSObject+LVSwizzle.h

@interface NSObject (LVSwizzle)

/**
 *  @brief  替換方法實現(xiàn)
 *
 *  @param srcSel 替換的方法
 *  @param tarClassName 被替換的方法的類名
 *  @param tarSel 被替換的方法
 */
+ (void)lv_swizzleMethod:(SEL)srcSel tarClass:(NSString *)tarClassName tarSel:(SEL)tarSel;

NSObject+LVSwizzle.m

#import "NSObject+LVSwizzle.h"
#import <objc/runtime.h>

@implementation NSObject (LVSwizzle)

+ (void)lv_swizzleMethod:(SEL)srcSel tarClass:(NSString *)tarClassName tarSel:(SEL)tarSel{
    if (!tarClassName) {
        return;
    }
    Class srcClass = [self class];
    Class tarClass = NSClassFromString(tarClassName);
    [self lv_swizzleMethod:srcClass srcSel:srcSel tarClass:tarClass tarSel:tarSel];
}

+ (void)lv_swizzleMethod:(Class)srcClass srcSel:(SEL)srcSel tarClass:(Class)tarClass tarSel:(SEL)tarSel{
    if (!srcClass) {
        return;
    }
    if (!srcSel) {
        return;
    }
    if (!tarClass) {
        return;
    }
    if (!tarSel) {
        return;
    }
    Method srcMethod = class_getInstanceMethod(srcClass,srcSel);
    Method tarMethod = class_getInstanceMethod(tarClass,tarSel);
    method_exchangeImplementations(srcMethod, tarMethod);
}

@end

創(chuàng)建UIViewController的分類UIViewController+BuryPoint

  • 瀏覽某個頁面就hook控制器的viewDidLoad方法
UIViewController+BuryPoint.m

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(lv_viewDidLoad);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        BOOL success = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) { // iOS 13.6崩潰找不到方法,最好判斷下
            class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)lv_viewDidLoad
{
    [self lv_viewDidLoad];
    
    // 如果是ViewController這個控制器就提交服務器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    }
}
  • 統(tǒng)計時長就hook viewDidAppear和viewDidDisappear
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(lv_viewDidAppear:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        BOOL success = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
        SEL originalSelector1 = @selector(viewDidDisappear:);
        SEL swizzledSelector1 = @selector(lv_viewDidDisappear:);
        Method originalMethod1 = class_getInstanceMethod([self class], originalSelector1);
        Method swizzledMethod1 = class_getInstanceMethod([self class], swizzledSelector1);
        BOOL success1 = class_addMethod([self class], originalSelector1, method_getImplementation(swizzledMethod1), method_getTypeEncoding(swizzledMethod1));
        if (success1) {
            class_replaceMethod([self class], swizzledSelector1, method_getImplementation(originalMethod1), method_getTypeEncoding(originalMethod1));
        } else {
            method_exchangeImplementations(originalMethod1, swizzledMethod1);
        }
    });
}

- (void)lv_viewDidAppear:(BOOL)animated
{
    [self lv_viewDidAppear:animated];
    
    // 可以在管理類里面定義個屬性記錄當前時間
    [BuryPointManager sharedInstance].time = CFAbsoluteTimeGetCurrent();
}
    
- (void)lv_viewDidDisappear:(BOOL)animated
{
    [self lv_viewDidDisappear:animated];

    // 根據(jù)頁面出現(xiàn)和消失計算瀏覽時間
    NSTimeInterval time = CFAbsoluteTimeGetCurrent() -  [BuryPointManager sharedInstance].time;
    // 這里可以根據(jù)是哪個控制器再提交服務器, 方法里面帶上時間和需要計算時長的頁面控制器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    } else if () {

    } ...
    // 如果每次判斷麻煩榛搔,可以在BuryPointManager里面定義一個viewControllers的數(shù)組來存需要埋點的控制器
    for (NSString *name in [BuryPointManager sharedInstance].viewControllers) {
        if ([name isEqualToString:NSStringFromClass([self class])]) {
            [[BuryPointManager sharedInstance] 方法];
            break;
        }
    }
}

三诺凡、點擊事件

點擊事件分為UIControl(UIButton)的點擊事件、手勢點擊事件药薯、UITableView和UICollectionView的點擊事件

  • UIControl(UIButton)的點擊事件
    直接hook UIControl的sendAction:to:forEvent:方法
UIControl+BuryPoint.m
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{    
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(lv_sendAction:to:forEvent:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        BOOL success = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
- (void)lv_sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event {
    [self lv_sendAction:action to:target forEvent:event];

    // 要區(qū)分具體控件绑洛,可以用tag值來區(qū)分,最好看接口需要用什么值來標識童本,可以把這個值設(shè)為控件的tag值真屯,如果標識是字母可以先轉(zhuǎn)成16進制再轉(zhuǎn)成10進制
    [[BuryPointManager sharedInstance] 方法];
}

  • 手勢點擊事件
    像一些控件添加手勢block就會觸發(fā),hook UITapGestureRecognizer的initWithTarget:action:addTarget:action:方法
UITapGestureRecognizer+BuryPoint.m

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(initWithTarget:action:);
        SEL swizzledSelector = @selector(countData_initWithTarget:action:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        BOOL success = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
        SEL originalSelector1 = @selector(addTarget:action:);
        SEL swizzledSelector1 = @selector(countData_addTarget:action:);
        Method originalMethod1 = class_getInstanceMethod([self class], originalSelector1);
        Method swizzledMethod1 = class_getInstanceMethod([self class], swizzledSelector1);
        BOOL success1 = class_addMethod([self class], originalSelector1, method_getImplementation(swizzledMethod1), method_getTypeEncoding(swizzledMethod1));
        if (success1) {
            class_replaceMethod([self class], swizzledSelector1, method_getImplementation(originalMethod1), method_getTypeEncoding(originalMethod1));
        } else {
            method_exchangeImplementations(originalMethod1, swizzledMethod1);
        }
    });
}

- (instancetype)countData_initWithTarget:(id)target action:(SEL)action {
    [self countData_initWithTarget:target action:action];
    [self addTarget:target action:action];
    return self;
}

- (void)countData_addTarget:(id)target action:(SEL)action {
    [self countData_addTarget:target action:action];
    [self countData_addTarget:self action:@selector(countData_trackTapGestureAction:)];
}

- (void)countData_trackTapGestureAction:(UITapGestureRecognizer *)sender {
    UIView *tapView =  sender.view;

    [[BuryPointManager sharedInstance] 方法];
}


  • UITableView和UICollectionView的點擊事件
    這類事件可以在UITableView和UICollectionView封裝的基類實現(xiàn)tableView:didSelectRowAtIndexPath: collectionView:didSelectItemAtIndexPath:方法進行埋點穷娱,也可以hook這兩個方法绑蔫。不過和上面的hook不同的是這兩個方法是代理方法,所以在替換這兩個方法前泵额,要先替換delegate的set方法setDelegate配深,在進行tableView:didSelectRowAtIndexPath: collectionView:didSelectItemAtIndexPath:的方法替換。其他別人封裝的控件同理嫁盲,像SDCycleScrollView的點擊方法篓叶,你也可以先替換它的setDelegate,再替換點擊代理方法cycleScrollView:didSelectItemAtIndex:,當然缸托,由于SDCycleScrollView內(nèi)部就是一個UICollectionView左敌,你最后也可以替換collectionView:didSelectItemAtIndexPath:
UITableView+BuryPoint.m

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(setDelegate:);
        SEL swizzledSelector = @selector(hook_setDelegate:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        BOOL success = class_addMethod([self class], originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod([self class], swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)hook_setDelegate:(id<SDCycleScrollViewDelegate>)delegate {
    SEL originalSelector = @selector(tableView:didSelectRowAtIndexPath:);
    SEL swizzledSelector = @selector(lv_tableView:didSelectRowAtIndexPath:);
    Method originalMethod_delegate = class_getInstanceMethod([delegate class], originalSelector);
    Method originalMethod_self = class_getInstanceMethod([self class], originalSelector);
    Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
    //若未實現(xiàn)代理方法俐镐,則先添加代理方法
    BOOL didAddOriginalMethod = class_addMethod([delegate class], originalSelector, method_getImplementation(originalMethod_self), method_getTypeEncoding(originalMethod_self));
    if (!didAddOriginalMethod) {
        //已經(jīng)實現(xiàn)代理方法矫限,則先將hook的方法添加到delegate class中,然后再交換Imp指針
        BOOL didAddSwizzledMethod = class_addMethod([delegate class], swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddSwizzledMethod) {
            Method newMethod = class_getInstanceMethod([delegate class], swizzledSelector);
            method_exchangeImplementations(originalMethod_delegate, newMethod);
        } else {
            method_exchangeImplementations(originalMethod_delegate, swizzledMethod);
        }
    }
    [self hook_setDelegate:delegate];
}

- (void)lv_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self lv_tableView:tableView didSelectRowAtIndexPath:indexPath];
    
    [[BuryPointManager sharedInstance] 方法];
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末佩抹,一起剝皮案震驚了整個濱河市叼风,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棍苹,老刑警劉巖无宿,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異枢里,居然都是意外死亡懈贺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門坡垫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人画侣,你說我怎么就攤上這事冰悠。” “怎么了配乱?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵溉卓,是天一觀的道長。 經(jīng)常有香客問我搬泥,道長桑寨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任忿檩,我火速辦了婚禮尉尾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘燥透。我一直安慰自己沙咏,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布班套。 她就那樣靜靜地躺著肢藐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吱韭。 梳的紋絲不亂的頭發(fā)上吆豹,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音,去河邊找鬼痘煤。 笑死凑阶,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的速勇。 我是一名探鬼主播晌砾,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烦磁!你這毒婦竟也來了养匈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤都伪,失蹤者是張志新(化名)和其女友劉穎呕乎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陨晶,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡猬仁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了先誉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片湿刽。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖褐耳,靈堂內(nèi)的尸體忽然破棺而出诈闺,到底是詐尸還是另有隱情,我是刑警寧澤铃芦,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布雅镊,位于F島的核電站,受9級特大地震影響刃滓,放射性物質(zhì)發(fā)生泄漏仁烹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一咧虎、第九天 我趴在偏房一處隱蔽的房頂上張望卓缰。 院中可真熱鬧,春花似錦砰诵、人聲如沸僚饭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳍鸵。三九已至,卻和暖如春尉间,著一層夾襖步出監(jiān)牢的瞬間偿乖,已是汗流浹背击罪。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贪薪,地道東北人媳禁。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像画切,于是被迫代替她去往敵國和親竣稽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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

  • 在博客開始之前霍弹,先說說目前多數(shù)開發(fā)者使用的幾種埋點方案 代碼復制黏貼到需要統(tǒng)計的類和方法中毫别,工作量大,不利維護 使...
    夢回藍橋閱讀 2,686評論 2 8
  • 一典格、可視化埋點 可視化埋點的出現(xiàn)岛宦,是為解決代碼埋點流程復雜、成本高耍缴、新開發(fā)的頁面(H5砾肺、或者服務端下發(fā)的 json...
    FFFF00閱讀 2,162評論 0 5
  • 1、背景 稀里嘩啦一大段 2防嗡、主要功能劃分 從整個流程來說变汪,我把他劃分為下面幾個主要的功能,事件攔截蚁趁、viewPa...
    林風098閱讀 982評論 2 7
  • 0 引言 最近在負責公司的HubbleData的埋點SDK的開發(fā)任務疫衩,產(chǎn)品的雛形其實在幾年前就已經(jīng)有了,公司內(nèi)部的...
    魯冰閱讀 9,938評論 9 61
  • GitHub項目地址 前言 最近業(yè)務需要加入一大批埋點統(tǒng)計事件荣德,這個頁面添加一點代碼那個頁面添加一點代碼,各個頁面...
    青年別來無恙閱讀 1,942評論 0 23