美文网首页 ios零碎记录平时生活和工作中的iOSiOS进阶
iOS动态性(二)可复用而且高度解耦的用户统计埋点实现

iOS动态性(二)可复用而且高度解耦的用户统计埋点实现

作者: 编程小翁 | 来源:发表于2016-04-11 16:36 被阅读15864次

    声明:本文是本人 编程小翁 原创,转载请注明。

    用户统计.jpeg
    用户行为统计(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.gif

    需求是

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

    1)统计两个页面的展示与离开次数

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

    埋点代码注入.jpg

    这时候问题来了,项目中每个页面都会有自己的页面事件编号(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"]) {
            pageEventID = bEnterPage ? @"EVENT_DETAIL_ENTER_PAGE" : @"EVENT_DETAIL_LEAVE_PAGE";
        }
        //else if (<#expression#>)...
    }
    

    当然,我们可以有更优雅的方式,比如用一个配置表替代上面一长串的if判断,这样无论页面数怎么增加,代码始终是那么一小段。我们新建一个WGlobalUserStatisticsConfig.plist的配置表来存放每个页面在进入以及离开时的pageEventID,结构如下:

    配置表结构.png

    因此,页面进出统计中获取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;
    }
    

    效果如下:

    页面埋点.gif

    以上就是完成了页面进出统计的埋点,并且达到了我们的第三点预期:对现有代码基本无影响。通过Method Swizzling的方式现有的工程甚至不需要import任何文件!后期代码变动时需要维护的仅仅是plist配置表。

    2)统计收藏、分享单击事件的次数

    与上一节思路一致,要做到解耦显然需要通过category+hook来实现。本文demo中收藏跟分享都是UIButton类型,可以考虑添加UIButton的catogory。但更好的方式是添加UIControl的category,这样可以让埋点代码覆盖到所有UIControl的子类中去,比如button、switch、segment等,提高复用性。
    既然要hook,那就要清楚到底要hookUIControl的哪(几)个方法,只有部分方法是满足埋点需求的,最好是所hook的方法能提供target、actionName等信息。这是个尝试的过程。
    UIControl的方法列表有以下:

    UIControl方法列表.png

    通过观察方法名和参数,我们有理由怀疑是倒数第二个,因其携带了不少貌似有价值的信息:

    - (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
    

    于是写出测试代码看看:

    @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

    可以看到,通过category+method swizzling的方式在没有修改现有工程任何代码的情况下已经成功Hook到所有点击事件,在Hook代码中我们知道了一个点击事件的target也就是ViewController,也知道了点击事件的响应函数名,知道了点击的TouchSet。这些信息已经能满足埋点需求了。
    与页面统计埋点类似,我们同样采用plist配置表的方式避免一大长串的if-else判断:

    单击事件配置表结构.png
    有了这张配置表就很容易得到某次单击事件的事件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,页面的进出等)、交互事件ID(ControlEventIDs,单击、双击、手势等)。分类有助于下文使用单元测试(Unit Test)进行自动化后期维护。

    埋点效果如图:

    单击埋点效果.gif

    到这里先做了阶段性的总结,本文提出的思路有以下优越性:

    • 与工程代码基本解耦,避免引入“脏代码”
    • 即使后期工程代码发生重构,需要修改的仅仅是plist配置表
    • 维护配置表比维护散落在工程各个角落的代码简单

    四、基于单元测试的后期维护

    俗话说,创业难守业更难。前面的思路基本可以完成初步的埋点需求。但是在实际项目中代码重构是很频繁的。这意味着在多人协作开发、代码重构频繁的项目中响应事件方法甚至页面名称都可能被改掉,造成事件ID获取不到导致埋点失效。
    代码变动的情况无非以下几种(这里只介绍响应事件发生改变的情况):

    1、响应事件方法名称改变或者删除

    比如收藏事件原先是onFavBtnPressed:,之后被改成onFavouriteBtnPressed:。代码发生变动但是plist配置表中由于开发人员疏忽忘记同步修改了。这种疏忽在开发压力大进度赶的情况下是有很大概率发生的。由于代码与配置表不匹配将导致eventID为nil。在这种情况下单元测试就很有必要了,使用完备的测试用例能在发版前检测到这种不匹配情况从而避免埋点失效。
    在单元测试中我们首先读取plist配置文件,遍历所有的页面。在一个页面内遍历所有的ControlEventIDs,对每个响应函数名进行respondsToSelector:判断:

    单元测试介绍.png

    单测代码如下:

    - (void)testIfUserStatisticsConfigPlistValid
    {
        NSDictionary *configDict = [self dictionaryFromUserStatisticsConfigPlist];
        XCTAssertNotNil(configDict, @"WGlobalUserStatisticsConfig.plist加载失败");
        
        [configDict enumerateKeysAndObjectsUsingBlock:^(NSString *  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            XCTAssert([obj isKindOfClass:[NSDictionary class]], @"plist文件结构可能已经改变,请确认");
            NSString *targetPageName = key;
            Class pageClass = NSClassFromString(targetPageName);
            id pageInstance = [[pageClass alloc] init];
            
            //一个pageDict对应一个页面,存放pageID,所有的action及对应的eventID
            NSDictionary *pageDict = (NSDictionary *)obj;
            
            //页面配置信息
            NSDictionary *pageEventIDDict = pageDict[@"PageEventIDs"];
            
            //交互配置信息
            NSDictionary *controlEventIDDict = pageDict[@"ControlEventIDs"];
            
            XCTAssert(pageEventIDDict, @"plist文件未包含PageID字段或者该字段值为空");
            XCTAssert(controlEventIDDict, @"plist文件未包含EventIDs字段或者该字段值为空");
            
            [pageEventIDDict enumerateKeysAndObjectsUsingBlock:^(NSString *  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
                XCTAssert([value isKindOfClass:[NSString class]], @"plist文件结构可能已经改变,请确认");
                XCTAssertNotNil(value, @"EVENT_ID为空,请确认");
            }];
            
            [controlEventIDDict enumerateKeysAndObjectsUsingBlock:^(NSString *  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
                XCTAssert([value isKindOfClass:[NSString class]], @"plist文件结构可能已经改变,请确认");
                NSString *actionName = key;
                SEL actionSel = NSSelectorFromString(actionName);
                XCTAssert([pageInstance respondsToSelector:actionSel], @"代码与plist文件函数不匹配,请确认:-[%@ %@]", targetPageName, actionName);
                
                //EVENT_ID不能为空
                XCTAssertNotNil(value, @"EVENT_ID为空,请确认");
            }];
        }];
        
    }
    

    我们来测试一下,如果把HomeViewControlleronFavBtnPressed:改成onMyFavBtnPressed:后单元测试的结果就是:

    单元测试不通过.png

    这种改变给单测轻松捕捉到了,

    只要XCTAssert的log够详细,维护起来其实相当轻松的。

    上图中的log已经明确指出-[HomeViewController onFavBtnPressed:]方法发生了改变。

    2、代码中新增了响应事件

    这种情况常见于新版本中有新的埋点需求。如果代码中新增了响应事件并且该响应事件是在PM要求的埋点列表中,但是plist有可能会漏掉该事件。这种情况是比较棘手的。上一种情况是基于plist列表去校验代码,这里就要反过来,根据代码去校验plist是否有缺失。但问题来了,一个项目中响应函数往往是非常多的,并不是任何响应函数都需要埋点。需要埋点的响应函数与其他响应函数并没有区别。
    对于这种情况,一种方式是加强code review避免忘记往配置表中添加埋点(这简直就是废话);一种是:要求埋点响应函数的方法名中包含约定的字符串,比如收藏事件的方法名为onFavBtnPressed_UA:表示这个事件是需要埋点的。然后在单元测试中使用运行时APIclass_copyMethodList取出标记了_UA的所有函数,随后到plist中校验是否存在。不存在则表示测试用例不通过,提示开发人员校验。

    代码略。如果对单元测试不熟悉,可以参考单元测试

    小总结:
    合理的单元测试可以为本文方案的后期维护减轻相当大的负担,测试用例的完备性很重要,需要用心设计考虑周全。

    五、结语

    以上就是结合运行时所设计出的用户统计思路全部内容。应该说该方案的可复用性与解耦程度都是不错的,既适合于新建的工程,也适合于已经创建的工程。看起来内容多,其实总结起来无非几个步骤:plist配置表+Hook+单元测试。利用Method Swizzling把埋点代码集中管理其实也是合理的,有利于专人开发、跟踪及维护。当然以上思路只考虑简单的情形,更复杂的情况就需要变通了,但总体思路就是如此。
    思路可能不完美,但作为一种尝试也未尝不可。路都是走出来的。

    本文demo地址,记得star噢!


    • 喜欢本文可以点一下喜欢关注我,或者留个言示个爱(抛媚眼中)
    • 不喜欢可以留言提建议,我必虚心接受
    • 欢迎转载

    相关文章

      网友评论

      • 水滴2014:获取ID不用plist.在appdelegate中设置currentController属性,在UIViewController的viewViewAppear中将currentController赋值为self,这样每到一个vc都可以获取当前的vc,拼接成字符串,赋值给ID就可以了.即使改变类的名字也没有关系,添加类名也没有关系
      • 0cb3faf338b6:请问:
        1.使用plist文件来维护埋点列表,不会出现性能问题吗?
        2.如果同一个控制器根据不同的业务类型来实现两个或者更多的页面,怎么来区分pageID(情况比较多的情况下)?
      • 娶什么名字呢:感谢分享
      • Bob_Running:UIEvent中通过actionString和targetName获取eventID,如果actionString和targetName都相同的话,就不知道取哪一个eventid了。比如,我用reactcocoa 给同一个控制器中的两个按钮添加事件时,两个事件的actionString和targetName就相等
        欧阳铨:@Bob_Running 我的想法是加上这个view在父view的index。但是要处理当某个view被removeSubview的时候index变化的情况。
        Bob_Running:求各位大神给更好的方法,获取eventid
      • 1cdb19c230be:要统计的button点击事件 在自定义cell内 我的plist表要怎么写呢 当点击的时候[target class] 肯定是tableviewcell 然后我单独拿出来写在和controller同级时 每次点击 都统计点击事件 和外层的controller 重复统计
      • 1cdb19c230be:你好,我想问一下你项目里的.h干啥用的 貌似没用啊
      • 1cdb19c230be:nice!! 正好最近要分享技术 就分享你这个思想吧 thank you very much
      • TsingQue:千万别用Plist文件去做这个,会坑死自己的。。。。
        0cb3faf338b6:因为性能消耗问题吗?
        雨落有归家:为什么?
      • 崇山峻岭:现在有一个问题, 我有一个封装的View, 里面有一个按钮,按钮时间的target就是当期View 这个view可能有是个地方用, 那么知道是在哪个页面点击的呢? 因为 action, 和 target 是一样的, 这个时候无法从配置列表(.plist)中单独获取事件ID, 请问这种情况有没有什么好的解决方案?
        方寸山_linyut:类是同一个,但对象有无数个,是不同的;比如都是华为手机,你的华为手机和我的华为手机是不一样的;也就是说plist里对这个action有不同的标识符
      • f1535a71d2d8:集成UMeng的时候,[MobClick endLogPageView:pageID] 这个方法调用了,但是并没有UMLOG;会导致页面统计出错。。。
      • 南方小金豆:textfield 的事件能hook到嘛? 应该怎么做
      • pinglife:如果我要是tableView:didSelectRowAtIndexPath:这个方法该怎么办?
      • wind黑子:文章很好,收获也很多。想请问作者对Aspects这个库的看法,直接用这个库去实现作者的打点思路的话是不是可行。
      • Pusswzy:简书转载抄袭遍地的情况下, 总会找到一些好的文章的
      • 羊肉泡馍啊:我们想要当前界面的一些参数,怎么获取
      • 加双芋:看完啦,正在搞这方面,感觉楼主写的很用心哦,赞一个!!:clap: 关于数据上传部分,考虑用户体验和流量,你们是怎么做的
        编程小翁:@木卜小兑 上传是需要缓存的,毕竟大部分场景下不需要实时统计。我们的处理是在下一次触发埋点时检查距离上次上传的时间差是否满足阈值,满足则上传;切前后台同样做一次上述判断。这些逻辑统计封装在Analytic模块中
      • 点亮橘子树:plist少了一个 My onMyFavBtnPressed:joy:
      • littlewish:作者写的这些大概思想上是没错的,但是这里没考虑业务的复杂性。很多打点的数据上报都避免不了和业务数据的耦合,所以要解决业务数据耦合的问题,大概有下面的解决方法,1、所有业务数据由开发者封装,然后上传,这个会给客户端逻辑带来很多打点代码。2、通过服务器下发业务数据组装规则,接口下发业务数据,通过打点方案内部去捕捉业务数据按规则组装上传。但是还是避免不了一些需要手动打点上传业务数据,如一些无法下发数据的打点等
        编程小翁:@littlewish :+1:
      • brave723:要统计的列表数据是根据服务器返回的,hook tableView的时候怎么拿到数据?
      • 原野de呼唤:有几个页面的ViewWillAppear方法勾不到,不知道是什么原因
        编程小翁:@原野de呼唤 :smile:
        原野de呼唤:忘记写[super viewWillAppear:animated];了
      • 标准答案:楼主, 你的demo有bug啊, 运行以后点击收藏的时候会crash
        编程小翁:@标准答案 我看下 有日子没更了
      • 云逸枫林:mark 支持一下,这个埋点比较轻量化,正常项目里可能还得做个数据库,不然每次页面跳转都网络上传有点麻烦,可以适当缓存,每次启动传递上一次的记录,不过这也只是我的一个建议。
        编程小翁:@云逸枫林 那必须的,任何埋点方案都是需要搭配缓存的,毕竟实时上传埋点会给用户尤其是使用移动流量的用户带来压力。这篇文章的重点是讨论埋点方案,并不包含上传部分。不过谢谢你的补充哈
      • piecs:大兄弟,你的这个貌似有点小坑啊。首先,再处理UIControl时,下载你的demo点击按钮会出现crash。然后,我修改了一下,但是还是无法解决按钮点击后无法响应,应该是runtime时方法被替代导致的。
        也许是我没有理解,但,真的不能用。求作者大神指点一下啊。
        编程小翁:是因为homeViewController的onFavBtnPressed在测试单测Case时被我改成onMyFavBtnPressed导致找不到方法,忘记改回去了,已更正,谢谢提醒
        编程小翁:@piecs 我看下 有日子没更新了
      • 涛大:谢谢分享
      • Bob林:统计 viewDidLoad 运行时间的话 就不准确的 这样直接交换
        编程小翁:影响不大 基本可认为是准确的
      • 简书12138:tableView:didSelectRowAtIndexPath:
        这个方法要怎么hook啊 楼主
      • 012ca90bd128:正在做这个,学习学习 :smiley:
      • GJCode:写的很棒,不过,想请教下,如果我想做到完全的自动化埋点方式,那么除了这种点击事件和ViewController的ViewWillAppear之类的事件使用Hook,那像手势这样的事件该如何Hook到
        GJCode:@superWWWWWWW 手势目前没有处理自动化去埋点
        1cdb19c230be:你好 手势类的hook 你做完了吗 请教一下 最近也在研究
      • 伍骁辛:请教一下,如果要是计算用户停留在一个页面的时间,就在页面出现和消失的方法加埋点就可以了么
        编程小翁:一般情况下是这样的,不过你还得考虑app被切后台的情况,还要再扣掉app处于inactive的时间
      • hitlerstar:请教一下,如果我们公司要做一款SDK,统计第三方APP的所有控件,可以实现么
        编程小翁:不是很理解你说的'统计第三方APP的所有控件',可以具体说说吗
      • 95c9800fdf47:那个hook 封装最好改成分类,要不然会出问题
        Pusswzy:出啥问题 你倒是说明白啊
        编程小翁:怎么说?WHookUtility只是封装静态方法,并不涉及任何实例,有什么问题呢
      • b96acac40a08:代理事件怎么搞啊
      • 神魔狼:发送给服务端也是走的http吗 这样不是到处都是网络请求 对性能有影响吗 :disappointed_relieved:
        编程小翁:这篇文章讲的是打点的方法,并不涉及缓存。每个打点肯定是要配合缓存逻辑的,`sendEventToServer`并非就是直接发给服务端,而是把数据交给缓存模块,至于以什么频率发送外部不必理会
      • 时间戳:收藏下,准备好好研究一下,写的很好
      • 雪_晟:大神,那这些日志怎么拿到呢 ,用什么存储呢
        编程小翁:@miss李manman 可以用友盟统计SDK
      • Java会一点:用途不是太大,同学们试试就知道了。
        kirito_song:你说的用处不太大、是收集之后没人看吧。
        Pusswzy:何出此言?
      • 7b3f15cccda8:统计的时候需要当前界面的一些参数,这里怎么解决
        云逸枫林:这个也可以通过class_copyIvarList获取所有的属性吧
        Java会一点:@GavinOY0123 统计的时候需要当前界面的一些参数,这里怎么解决
        Java会一点:@GavinOY0123同样的问题 求解决
      • niuxinghua:+load里面还要dispatchonce么 那个不是只执行一次么?
        Pusswzy:@niuxinghua 可以主动调用load方法
        niuxinghua:@编程小翁 哈哈 可以。
        编程小翁:@niuxinghua 是执行一次 弄个双保险:smile:
      • f4d12035e020:请问如何统计cell的点击呢?
        从来吃不胖:@冰川的眼泪 交换tableView的设置代理方法。详情:http://www.jianshu.com/p/d96973e15e4a
      • 大风起兮_Seven:小翁,最近在做用piwik来采集用户行为,根据你写的里面是用方法交换可以监听到所有button的点击事件,但是在实际项目中,是根据tag值的不同进行不同的处理,应该怎么做呢
        从来吃不胖:@你从我的全世界路过 tag是个属性。直接过去按钮的tag是比较难。我的意思是,根据你需要的业务编写对应的value,不用给每个button添加tag再去判断。因为你在方法交换里,获取到的只是按钮上添加的action-target而已,而没有按钮
        你从我的全世界路过:@从来吃不胖 请问程序内部怎么获取到tag值,从而来取值呢?
        从来吃不胖:@Seven_zhuang 在那张“配置表”里,可以加入tag对应的key-value值。在button点击事件里判断“配置表”里有没有加入tag对应的key-value值,若有加入,根据此时这个button的tag去记录。
      • SOI:BOOL didAddMethod =
        class_addMethod(class,
        originalSelector,
        method_getImplementation(swizzledMethod),
        method_getTypeEncoding(swizzledMethod));
        这个方法有什么用 为啥不是直接使用method_exchangeImplementations(originalMethod, swizzledMethod);?
        编程小翁:抱歉到现在才回,这一年太忙了。这边是容错逻辑,既然要exchange那就得保证类有存在originalMethod,否则有crash的危险。如果已经有originalMethod了则class_addMethod方法将失效并返回NO,这时我们就放心地调用exchangeImplementations方法了
      • csqingyang:赞!
      • wh5865885:大神~我们用的 google analytics 里面涉及的参数特别多...种类也不一样...这个时候UIButton 的扩展似乎就不适用于我这个 请问下有什么好的办法吗
      • Arnold134777:确实不错,不过我有疑问,如果我统计的数据是根据服务器返回数据决定的,那好像这种方式不行
      • fa87afbcacef:要是传参呢,手势呢
      • Kean_Qi:不错 值得关注一下
        编程小翁:@QZYOS :smile: :smile:
      • 开发者测试帐号:思路很棒, 我准备按这个思路重构一下,看到你这篇文章, 特地注意一个简书帐号表示一下感谢!
        编程小翁:@开发者测试帐号 以后多多交流
      • TDB:确实大神,我问点低级的问题,一直想找个MAC上录屏保存gif图片的工具,大神能告诉一下你用的什么吗?
        TDB:@编程小翁 谢谢啊:smile:
        编程小翁:@TDB 我用的是licecap,免费的
      • 名扬丶四海:谢谢分享,最近也在做统计,很多东西都要加,看了下你的介绍,有点思路了。
        编程小翁:@iOS_Country :smile::smile:
      • 缱绻一时:我就想知道
        - (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];
        }

        类似于这段。自己给自己发消息。这样不就死循环了么?求大神指导。
        缱绻一时:@编程小翁 谢谢大神指导。
        编程小翁:@缱绻一时 不会的。在+load函数里-[swiz_sendAction: to: forEvent:]与-[sendAction: to: forEvent:]两个方法的实现已经被交换了,所以[self swiz_sendAction:action to:target forEvent:event];这句实际走的是-[sendAction: to: forEvent:]的实现。建议你看看我以前在博客园的文章http://www.cnblogs.com/wengzilin/p/4704996.html 里面有图,可能会清晰点。这点理解了,method swizzling就基本没问题了
      • 陆大胖:统计很少和业务是分开的,业务的统计如果都是非关联的统计还好,如果一个复杂的关联统计那么你要怎么去swizzle?怎么保证你的代码就易读易维护?method swizzle 和hook代码本身就是阅读障碍和bug的来源。我们要抽离的仅仅只是统计模块关于统计数据存储和发送的部分,业务部分由上层自己写着,这样一点都不难维护,其实一开始的WUSerStatistics就已经比较好了
        Unicodeo:@编程小翁 埋点代码怎么复用。。如果埋点需要传递与业务相关的数据,并不是固定的那些id怎么做
        编程小翁:@陆大胖 看法不一样吧,埋点跟业务杂糅在一起会影响埋点代码复用性。当然,在关联统计这个问题上很难做到既保持高复用性又保持低耦合,这时候的hook代码就需要更复杂一些。本文只是抛砖引玉,开阔思路,用配置表以及单元测试降低维护难度。具体问题还是要具体分析的
      • 偌上:请问下想UIView和UIImageView的添加的手势,怎么做统计呢,怎么去hook
        荼白的巡礼之年:@kissmore 我尝试了2次method swizz,但是失败了,action调用的瞬间无法hook到,请问你解决了吗
        4d25b3b85d24:@编程小翁 获取到函数名怎么处理呢?怎么获取这个函数调用
        编程小翁:@偌上 作为hook解决埋点的第一期,主要解决点击类跟页面类的埋点,毕竟这些在埋点需求中一般占比大。至于手势的hook埋点就需要绕一些,直接hook手势响应函数是行不通的,而是需要hook手势响应的添加函数initWithTarget:以及addTarget:函数,获取到响应函数名。知道函数名就好办了,跟点击类的处理差不多。同样能实现低成本复用以及低耦合
      • 96e99d4978ab:不错,收藏了。
        我觉得重要的是通过配置表和单元测试的方式,以前还真没怎么想过。
        现在看来确实是个很不错的方案。
        赞!
        编程小翁:@yardanramon :smile:
      • 小强七号:刚好在弄埋点
        编程小翁:@小强七号 :smiley:
      • LV大树:nice ,非常。
        编程小翁:@为什么呢 :smiley:
      • 花前月下:不错。
        编程小翁:@花前月下 :smiley:
      • 夏都:如果我要统计的值跟另外的状态有关怎么办
        夏都:@编程小翁 我在想我没有表达清楚,我在点击按钮a时我需要根据控件b当前的状态来做区分
        编程小翁:@夏都 你关心的应该是你要统计的值的变化,至于那个值与什么状态有关应该不需要关心,因为最终仍旧体现在统计值的变化上。你说的这种埋点就应该单独建一个工具类,然后用KVO观察你要的值,在工具类内做上传处理。这样同样能做到与原工程解耦,高解耦意味着高复用性,移到其他工程依旧可用。不知道这样说你是否明白
      • da27c260cc85:哎呦,不错呦,公交上看到,到了公司详细看一下
        Kean_Qi:写的真心不错
        da27c260cc85:@编程小翁 在重构代码, 想到这里, 简直nice呀. 写到我心坎里去了
        编程小翁:@Vassily :smile: :smile:

      本文标题:iOS动态性(二)可复用而且高度解耦的用户统计埋点实现

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