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> 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<GAITracker> 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> aspectInfo) {
//将参数发到自己服务器
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
id<GAITracker> 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<AspectToken>)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却是一样的,这就很好的解决了问题。
实在不好这样埋的部分埋点,可以选择方法一进行埋点。