iOS 埋点

所谓埋点就是App端采集用户某些事件的数据,提交到服务器进行分析。那某些事件是指哪些事件呢?譬如App的启动和退出、浏览某个页面、一些的点击事件等等。项目中需要埋点一般用全埋点,何为全埋点?即是指无需应用程序开发工程师写代码或者只写少量的代码,即可预先自动收集用户的所有或者绝大部分的行为数据,然后再根据实际的业务分析需求从中筛选出所需行为数据并进行分析。下面是我项目中做的几种事件埋点。

一、App的启动和退出

直接在AppDelegate实现对应的方法

回调方法 本地通知 通知时机
- applicationDidBecomeActive: UIApplicationDidBecomeActiveNotification 从后台已经进入前台
- applicationDidEnterBackground: UIApplicationDidEnterBackgroundNotification 已经进入后台
- application:didFinishLaunchingWithOptions: UIApplicationDidFinishLaunchingNotification 进入程序
- applicationWillTerminate: UIApplicationWillTerminateNotification 将退出程序
AppDelegate.m
- (void)applicationWillTerminate:(UIApplication *)application
{
    // 将要退出程序,调用对应接口
}

可以创建一个埋点管理类BuryPointManager,用来提交数据给服务器

AppDelegate.m
- (void)applicationWillTerminate:(UIApplication *)application
{
    // 将要退出程序,调用对应接口
    [[BuryPointManager sharedInstance] 方法];
}

如果有SceneDelegate,applicationDidBecomeActive:applicationDidEnterBackground:等方法在AppDelegate实现不了(application:didFinishLaunchingWithOptions:applicationWillTerminate:还是在AppDelegate实现),要去SceneDelegate实现,applicationDidBecomeActive:applicationDidEnterBackground:对应SceneDelegate的sceneDidBecomeActive:sceneDidEnterBackground:

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

二、页面浏览事件

页面浏览事件分浏览某个页面和统计某个页面的浏览时长

方法一:用视图控制器基类埋点

一般项目创建都会创建一个UIViewController的子类当页面的基类,如叫BaseViewController,那么可以到这个类里面进行埋点

  • 浏览某个页面
BaseViewController.m
- (void)viewDidLoad{
    [super viewDidLoad];

    // 如果是ViewController这个控制器就提交服务器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    }
}
  • 统计时长
BaseViewController.m

// 不能用viewWillAppear和viewDidDisappear或者viewDidAppear和viewWillDisappear统计,如viewWillAppear和viewDidDisappear,因为下一个页面的viewWillAppear会比上一个页面的viewDidDisappear触发的快,因此[BuryPointManager sharedInstance].time会重置
- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear: animated];
    // 可以在管理类里面定义个属性记录当前时间
    [BuryPointManager sharedInstance].time = CFAbsoluteTimeGetCurrent();
}

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

    // 根据页面出现和消失计算浏览时间
    NSTimeInterval time = CFAbsoluteTimeGetCurrent() -  [BuryPointManager sharedInstance].time;
    // 这里可以根据是哪个控制器再提交服务器, 方法里面带上时间和需要计算时长的页面控制器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    } else if () {

    } ...
    // 如果每次判断麻烦,可以在BuryPointManager里面定义一个viewControllers的数组来存需要埋点的控制器
    for (NSString *name in [BuryPointManager sharedInstance].viewControllers) {
        if ([name isEqualToString:NSStringFromClass([self class])]) {
            [[BuryPointManager sharedInstance] 方法];
            break;
        }
    }

}

方法二:使用hook

首先先写一个写个分类实现方法替换

NSObject+LVSwizzle.h

@interface NSObject (LVSwizzle)

/**
 *  @brief  替换方法实现
 *
 *  @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

创建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] 方法];
    }
}
  • 统计时长就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];

    // 根据页面出现和消失计算浏览时间
    NSTimeInterval time = CFAbsoluteTimeGetCurrent() -  [BuryPointManager sharedInstance].time;
    // 这里可以根据是哪个控制器再提交服务器, 方法里面带上时间和需要计算时长的页面控制器
    if ([self isKindOfClass:[ViewController class]]) {
         [[BuryPointManager sharedInstance] 方法];
    } else if () {

    } ...
    // 如果每次判断麻烦,可以在BuryPointManager里面定义一个viewControllers的数组来存需要埋点的控制器
    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];

    // 要区分具体控件,可以用tag值来区分,最好看接口需要用什么值来标识,可以把这个值设为控件的tag值,如果标识是字母可以先转成16进制再转成10进制
    [[BuryPointManager sharedInstance] 方法];
}

  • 手势点击事件
    像一些控件添加手势block就会触发,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封装的基类实现tableView:didSelectRowAtIndexPath: collectionView:didSelectItemAtIndexPath:方法进行埋点,也可以hook这两个方法。不过和上面的hook不同的是这两个方法是代理方法,所以在替换这两个方法前,要先替换delegate的set方法setDelegate,在进行tableView:didSelectRowAtIndexPath: collectionView:didSelectItemAtIndexPath:的方法替换。其他别人封装的控件同理,像SDCycleScrollView的点击方法,你也可以先替换它的setDelegate,再替换点击代理方法cycleScrollView:didSelectItemAtIndex:,当然,由于SDCycleScrollView内部就是一个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);
    //若未实现代理方法,则先添加代理方法
    BOOL didAddOriginalMethod = class_addMethod([delegate class], originalSelector, method_getImplementation(originalMethod_self), method_getTypeEncoding(originalMethod_self));
    if (!didAddOriginalMethod) {
        //已经实现代理方法,则先将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] 方法];
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 在博客开始之前,先说说目前多数开发者使用的几种埋点方案 代码复制黏贴到需要统计的类和方法中,工作量大,不利维护 使...
    梦回蓝桥阅读 2,637评论 2 8
  • 一、可视化埋点 可视化埋点的出现,是为解决代码埋点流程复杂、成本高、新开发的页面(H5、或者服务端下发的 json...
    FFFF00阅读 2,067评论 0 5
  • 1、背景 稀里哗啦一大段 2、主要功能划分 从整个流程来说,我把他划分为下面几个主要的功能,事件拦截、viewPa...
    林风098阅读 915评论 2 7
  • 0 引言 最近在负责公司的HubbleData的埋点SDK的开发任务,产品的雏形其实在几年前就已经有了,公司内部的...
    鲁冰阅读 9,723评论 9 60
  • GitHub项目地址 前言 最近业务需要加入一大批埋点统计事件,这个页面添加一点代码那个页面添加一点代码,各个页面...
    青年别来无恙阅读 1,880评论 0 23