iOS动态性(五)一种可复用且解耦的用户统计实现(运行时Runtime)

为了达到更好的阅读效果,强烈建议跳转到这里查看文章。

iOS动态性是我的关于iOS运行时的系列文章,由浅入深,从理论到实践。本文是第5篇。有兴趣可以看看我之前的文章。

用户行为统计(User Behavior Statistics, UBS)一直是移动互联网产品中必不可少的环节,也俗称埋点。在保证移动端流量不会受较大影响的前提下,PM们总是希望埋点覆盖面越广越好。目前常规的做法是将埋点代码封装成工具类,但凡工程中需要埋点(如点击事件、页面跳转)的地方都插入埋点代码。一旦项目越来越复杂,你会发现埋点的代码散落在程序的各个角落,不利于维护以及复用。本文旨在探讨利用iOS的运行时机制实现一种可复、解耦、容易维护的用户统计方案。探讨毕竟是探讨,欢迎到留言讨论。本文虽有些长却是用心之作,希望你有耐心看完。

注:本文需要一些iOS的Runtime基础

该方案的完成将会用到以下知识:

  • Method Swizzling(Hook)
  • 单元测试

一、常规埋点做法

接着开头的话题,我们先回顾一下主流的埋点是怎么做的。我粗糙地将埋点分为两种:1、页面统计,包括页面停留时间、页面进入次数;2、交互事件统计,包括单击、双击、手势交互等。

1)常规页面统计埋点

以统计页面进入次数为例,最简单粗暴的做法是在所有页面的viewDidAppear:以及viewDidDisappear:中分别埋点,将自己对应的pageID上传给服务端。代码大概长酱紫:

@implementation HomeViewController
//...other methods
- (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:]封装网络请求,将ID上传给服务器。上述方案有以下弊端:

1、复用性差。这部分埋点代码很难给其他项目复用
2、工作量大。尤其当页面较多时,需要修改的代码较多
3、引入“脏代码”,不易维护

第3点提到的“脏代码”意思是用户行为分析这种业务其实跟主业务没太大关系,不应该保持如此高的耦合度,因为这些代码会干扰我们对项目主业务的维护。这个我个人看法。

2)常规交互事件埋点

常规做法一般在交互事件的selector中获取该事件的ID并上传给服务端,代码大概长酱紫:

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

稍微大一点的APP如果采用这种方式,那诸如此类的埋点代码将遍地都是。它的缺点参考页面统计埋点部分,其复用性基本为零,也就是在新项目中根本无法复用埋点代码。

小总结一下,采用常规的做法虽然直观方便,但在可复用性、可维护性等方面有所欠缺。在我看来,借助运行时可以很好地避开这些缺点。

二、Method Swizzling、Hook与代码注入

由于Runtime知识不属于本文的重点,这里只简单介绍。
在iOS中,我们可以在运行时替换两个方法的实现,达到“勾住”某个方法并注入代码的目的。具体做法是:

重载类的“+(void)load”方法,在程序加载到内存时利用Runtime的method_exchangeImplementations等接口将方法(设为M)的实现互相交换。当方法M被调用时就会被勾住(Hook),执行我们的方法。

这种技术也称为Method Swizzling,属于面向切面编程(Aspect-Oriented Programming)的一种实现。

替换两个方法的实现,代码一般长酱紫:

@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工具类下文会用到。比如现在我们要勾住UIViewControllerviewWillAppear:方法,可以这样做:

@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];
    });
}
#pragma mark - Method Swizzling
- (void)swiz_viewWillAppear:(BOOL)animated
{
    //插入需要执行的代码
    NSLog(@"我在viewWillAppear执行前偷偷插入了一段代码");
    //不能干扰原来的代码流程,插入代码结束后要让本来该执行的代码继续执行
    [self swiz_viewWillAppear:animated];
}
@end

更多关于Runtime、method swizzling、面向切面编程的介绍请参考这里

三、基于运行时的埋点方案

为了便于下文叙述,先引入一个简单的项目,共有两个页面(HomeViewControllerDetailViewController),如下:

需求是

  1. 统计两个页面的展示与离开次数
  2. 统计收藏、分享单击事件的次数
  3. 对现有工程代码影响越小越好
1)统计两个页面的展示与离开次数

这部分应该比较直观了,摒弃掉在每个controller中埋点的方式,我们对UIViewController添加category从而Hook到viewWillAppear:viewWillDisappear:。在这两个方法中注入埋点代码:

这时候问题来了,项目中每个页面都会有自己的页面事件编号(pageEventID),此处的埋点代码如何知道要发送什么pageEventID给服务端呢?轻松祭出if-else神器:

- (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"]) {
        pageEven
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值