统计打点是 App 开发里很重要的一个环节,App 的运行状态、改版后的效果、用户的各种行为等都需要打点,市面上也有不少可供选择的第三方库。 假设产品有这么个需求:当用户在详情页点击购买按钮时,记录一下事件。我们实现起来大概会是这样
// DetailViewController.m
- (void)onBuyButtonTapped:(UIButton *)button
{
// do some stuff, maybe send a request to server
[XXXAnalytics event:kSomeEventYouDefined];
}这个需求就这样轻松搞定了,但细细想想还是有不少问题的:
1.页面上会有其他的 Button,可能每个 Button 都要放上这么一段代码。
2.这些统计其实跟具体的业务无关,没必要跟业务代码混杂在一起,不优雅。
3.当改版或者重构时,有可能忘了把相应的打点代码迁移过去。
所以需要一种更好的方式来做这件事,这就是使用 AOP(Aspect-Oriented-Programming),翻译过来就是「面向切面编程」
通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
简单来说,就是可以动态的在函数调用的前后插一段代码。iOS 可以使用 Pete Steinberger 开发的 Aspects 这个库,大致原理是在 runtime 层,通过 swizzle method 来实现的。
来看一个小 Demo
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(idaspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];这样在 UIViewController 的 viewWillAppear: 被调用后,还会再调一下我们定义的 Block,这段日志就会被输出。而打点正好符合这种场景:正事干完之后,额外干一些跟业务无关的事情。
上面的例子,我们通过 AOP 来做的话,大概就是这样
// DetailViewController.m
- (void)onBuyButtonTapped:(UIButton *)button
{
// do some stuff, maybe send a request to server
// no need to call [XXXAnalytics event:]
}
// AppDelegate.m
- (void)setupAnalytics
{
[DetailViewController aspect_hookSelector:@selector(onBuyButtonTapped:) withOptions:AspectPositionAfter usingBlock:^(idaspectInfo, BOOL animated) {
[XXXAnalytics event:kSomeEventYouDefined];
} error:NULL];
}这样统计代码就从业务代码中剥离出来了。但是又产生了一个新问题,多个 Button Event,岂不是要写很多行这样的代码,「重复」这样的事情,作为一个程序员怎么能忍,简单,造一个方法
- (void)trackEventWithClass:(Class)klass selector:(SEL)selector event:(NSString *)event
{
[klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(idaspectInfo, BOOL animated) {
[XXXAnalytics event:event];
} error:NULL];
}使用起来就像这样
- (void)setupAnalytics
{
[self trackEventWithClass:DetailViewController selector:@seletor(onBuyButtonTapped:) event:kSomeEventYouDefined];
[self trackEventWithClass:ListViewController selector:@seletor(followButtonTapped:) event:kAnotherEventYouDefined];
// ...
}看起来又干净了些。这时,产品经理又提了个需求:当这个按钮点击时,如果已经登录了,发送 EventA,如果没有登录则发送 EventB,也就是说,不再只是 [XXXAnalytics event:] 这么简单了,还需要加上额外的逻辑,这也难不倒我们,加上一个 block 即可。
|