• 叙利亚避谈战中国是否摆大巴 人员严重不整花式缺阵 2018-05-11
  • 男子嫖娼后恋上卖淫女 女子外出约会被其砍杀 2018-05-11
  • 郭家耀:料指数于23200至23800点水平徘徊 2018-05-05
  • 海外热身天津遭惨败 队长:跟高水平打才能学习 2018-05-05
  • 中国首个海外基地为何选吉布提?扼守全球最重要航道 2018-05-05
  • 张稀哲再示爱新婚妻子:遇到对的人 有你就知足 2018-05-04
  • 崔龙洙称为输球会做自我反省 解拉米为何没进首发 2018-05-04
  • 特朗普打破惯例 罕见“优待”中国记者 2018-05-04
  • 英国脱欧后都柏林能否成为欧洲新金融中心 2018-05-03
  • 英国明年3月底前将启动脱欧程序 脱欧时间表一览 2018-05-03
  • 加拿大“网红”柯基因病辞世 粉丝超70万 2018-05-03
  • BFS牛汇:菲律宾政局动荡 比索面临贬值压力 2018-05-02
  • 哈勃望远镜捕捉到垂死恒星:如蝴蝶蹁跹(图) 2018-05-02
  • 揭秘:小检察官如何替人“消灾” 买通公安法院 2018-05-02
  • 爱神罕见粗口回应外界质疑:老子一点都不在乎 2018-05-01
  • 36选7玩法 >iOS开发

    懒人做开发系列:利用Object-C特性埋点

    2018-04-24 14:18 编辑: suiling 分类:iOS开发 来源:moonCoder

    原文

    Objective-C是一门简单的语言,95%是C。只是在语言层面上加了些关键字和语法。真正让Objective-C如此强大的是它的运行时。它很小但却很强大。它的核心是消息分发。

    运行时会发消息给对象。一个对象的class保存了方法列表。那么这些消息是如何映射到方法的,这些方法又是如何被执行的呢?第一个问题的答案很简单。class的方法列表其实是一个字典,key为selectors,IMPs为value。一个IMP是指向方法在内存中的实现。很重要的一点是,selector和IMP之间的关系是在运行时才决定的,而不是编译时。这样我们就能玩出些花样。

    这次我们就是利用运行时来进行配置化的埋点。首先说下什么是埋点:所谓埋点就是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,包括访问(Visits),访客(Visitor),停留时间(Time On Site),页面查看(Page Views,又称为页面浏览)和跳出率(Bounce Rate,又可称为蹦失率)。这样的信息收集可以大致分为两种:页面统计(track this virtual page view),统计操作行为(track this button by an event)。

    这种的正常做法就是在各自的页面的viewWillAppear以及按钮的点击实现里去加代码传输数据给服务端进行统计,这种方式虽然省脑子,但是既耗时间,也不便于后期维护。

    利用语言的特性我们对这种方式进行改进,首先我们要用到Aspects框架,Aspects是iOS平台一个轻量级的面向切面编程(AOP)框架,只包括两个方法:一个类方法,一个实例方法。核心原理就是:

    下面我们来看下实现:首先需要新建一个plist把你需要的埋点都加进去:

    然后看下代码实现:

    - (void)trackEvent {
       // Hook viewcontroller
       NSString *filePath = [[NSBundle mainBundle] pathForResource:@"KZWList" ofType:@"plist"];
       NSDictionary *configs = [NSDictionary dictionaryWithContentsOfFile:filePath];
       
       [UIViewController aspect_hookSelector:@selector(viewWillAppear:)
                                 withOptions:AspectPositionAfter
                                  usingBlock:^(id aspectInfo) {
                                      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                          NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                          NSString *pageImp = configs[className][@"KZWTrackPageName"];
                                          if (pageImp) {
                                              id tracker = [[GAI sharedInstance] defaultTracker];
                                              [tracker set:kGAIScreenName value:pageImp];
                                              [tracker send:[[GAIDictionaryBuilder createScreenView] build]];
                                          }
                                      });
                                  } error:NULL];
                                  
       // Hook Events
       for (NSString *className in configs) {
           Class clazz = NSClassFromString(className);
           NSDictionary *config = configs[className];
           NSString *pageImp = configs[className][@"KZWTrackPageName"];
           if (config[@"KZWTrackEvents"]) {
               for (NSDictionary *event in config[@"KZWTrackEvents"]) {
                   SEL selekor = NSSelectorFromString(event[@"KZWEventSelector"]);
                   [clazz aspect_hookSelector:selekor
                                  withOptions:AspectPositionAfter
                                   usingBlock:^(id aspectInfo) {
                                       //将参数发到自己服务器
                                       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                       id tracker = [[GAI sharedInstance] defaultTracker];
                                       [tracker send:[[GAIDictionaryBuilder createEventWithCategory:pageImp
                                                                                             action:event[@"KZWEventAction"]
                                                                                              label:event[@"KZWEventName"]
                                                                                              value:nil] build]];
                                           });
                                   } error:NULL];
                                   
               }
           }
       }
    }

    下面我们来说说该方案的缺陷:

    1、并不是所有的事件都是有继承自UIControl的控件来发出的,比如:手势,点击Cell。

    2、并不是所有的按钮点击了之后就立马需要埋点上传?可能在按钮的响应方法中经过了层层的if(){ } else{ }最后才需要埋点。

    3、如果有参数

    4、对于代理方法该怎样处理?

    5、如果很多个按钮对应着一个事件该怎样处理?

    6、项目中事件的处理方法不尽相同,方法的参数个数不一样,并且方法的返回值也不一样,如何对他们进行统一的处理?

    下面我们来一一解决这些问题。

    问题1:对于不是来自UIControl的子类发出的事件,我们一样是可以进行hooK,只不过方法有所不同。我们在UIControl的分类中写了一段嵌入的代码,确实hook住了系统UIButton的点击事件,是因为UIButton自身会调用UIControl的这个方法。但是对于点击事件,这个是我们自己写的一个方法,它的父类UIViewController中是没有的,所以在执行我们自己点击事件的方法时UIViewController分类中要嵌入的方法是不会被调用的,这时候怎么办,我们可以动态的给我们自己要hook的ViewController动态的添加一个方法,然后就可以hook了(这一点不太好理解)。具体的添加方法,可以参考本文的实例代码。

    问题2:对于是否上传和具体的业务逻辑相关的情况,我们可以用方法所在类的一个属性值进行标记,这个属性写在.m文件中即可(KVC可以获取.m文件中的属性值。),我们先执行要hook那个类的方法,然后根据plist中配置的相关标记进行相应的处理(这里的属性值其实也是不必要的,我么可以根据类名和方法名字符串的哈希生成唯一的key,然后利用runtime自动关联到这个类的mf_condition属性上,这个属性是一个字典其key就是刚才生成的,value就是运行完这个方法之后得到的值,然后这个值再跟plist中的配置做以比较)。

    问题3:对于和事件所在类有紧密关联的埋点数据,比如某个页面对应的产品ID,比如某个页面点击了cell,之后这个cell对应的model的ID。这个时候我们可以参考方法2,添加一个属性,用一个属性值来存储这些这些需要上传的具体数据。

    问题4:代理方法和手势的处理也是一样的,既然一个类实现了某个代理方法,那么其[someInstance respondsToSelector:someSelector]所返回的BOOL值应该是YES的,然后其它的就和手势的处理是一样的了。

    问题5:对于很多按钮对应一个响应事件的情况,我们可以利用RunTime动态的给按钮添加一个属性,比如:buttonIdentifier,这样我们就可以在plist中进行相应的配置,以进行相应的埋点处理。

    问题6:这个问题其实就是hook住所有的方法,然后给他们添加同一个代码段的问题,这时候我们可以使用Aspects这个第三方框架:

    + (id)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error {
    return aspect_add((id)self, selector, options, block, error);
     }

    调用这个接口,因为在UIViewController的分类中调用这个接口的对象不一样,并且我们根据plist中的配置hook的selector不一样,然而最后执行的block却是一样的,这就很好的解决了问题。

    实在不好这样埋的部分埋点,可以选择方法一进行埋点。

    作者:moonCoder

    链接:https://www.jianshu.com/p/0b38d4041886

    搜索CocoaChina微信公众号:CocoaChina
    微信扫一扫
    订阅每日移动开发及APP推广热点资讯
    公众号:
    CocoaChina
    我要投稿   收藏文章
    上一篇:iOS中Block的用法,举例,解析与底层原理(这可能是最详细的Block解析)
    下一篇:iOS进阶补完计划--通读runtime

    相关资讯

    我来说两句
    发表评论
    您还没有登录!请登录注册
    所有评论(0

    综合评论

    相关帖子

    sina weixin mail 回到顶部