打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

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

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

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Method Swizzling 和 AOP 实践
iOS 逆向-非越狱手机Hook App
iphone开发常用代码
Xcode常用编译选项设置
在Hackintosh上啟用和禁用系統完整性保護(SIP)
js插件开发规范
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服