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 却是一样的, 这就很好的解决了问题.
来源: http://www.tuicool.com/articles/32u2Qbn