美文网首页
iOS 埋点

iOS 埋点

作者: cocoaCoffee | 来源:发表于2022-08-19 20:26 被阅读0次

    所谓埋点就是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] 方法];
    }
    
    

    相关文章

      网友评论

          本文标题:iOS 埋点

          本文链接:https://www.haomeiwen.com/subject/uemnwrtx.html