统计埋点现状:
日志统计逻辑避免不了和业务逻辑搅合在一起
由于业务逻辑频繁的修改,会导致日志统计的修改不及时和遗漏
日志统计的维护在每次迭代开发后都是一次耗时不短的任务
AOP+HOOK实现统计代码的分离
AOP(Aspect Oriented Programming)面向切面编程
相比传统的OOP来说,OOP的特点在于它可以很好的将系统横向分为很多个模块(比如通讯录模块,聊天模块,发现模块),每个子模块可以横向的衍生出更多的模块,用于更好的区分业务逻辑。而AOP其实相当于是纵向的分割系统模块,将每个模块里的公共部分提取出来(即那些与业务逻辑不相关的部分,如日志,用户行为等等),与业务逻辑相分离开来,从而降低代码的耦合度。
AOP主要是被使用在日志记录,性能统计,安全控制,事务处理,异常处理几个方面。
在iOS中我们通常使用Method Swizzling(俗称iOS黑魔法)来实现AOP,Method Swizzling其实就是一种在Runtime的时候把一个方法的实现与另一个方法的实现互相替换。*
Aspects 一个基于Objective-C的AOP开发框架
It allows you to add code to existing methods per class or per instance, whilst thinking of the insertion point e.g. before/instead/after. Aspects automatically deals with calling super and is easier to use than regular method swizzling
它实现了在每个类或实例已存在的方法中添加代码,同时考虑到了代码插入的时机,如方法执行前、替换已有方法的实现代码、方法执行后,并且能自动处理父类调用,比iOS普通的method swizzling更简单易用
它提供了以下方法
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Deregister an aspect.
/// @return YES if deregistration is successful, otherwise NO.
id<AspectToken> aspect = ...;
[aspect remove];
用户行为统计代码的分离
首先,这种配置性代码必然放在AppDelegate中,既然实现分离,我们可以通过AppDelegate+StaticsConfig类目的方式,实现单独维护一个统计类,直接上代码
static NSString *GLLoggingPageImpression = @"pageName";
static NSString *GLLoggingTrackedEvents = @"pageEvent";
static NSString *GLLoggingEventName = @"eventName";
static NSString *GLLoggingEventSelectorName = @"eventSelectorName";
static NSString *GLLoggingEventHandlerBlock = @"eventHandlerBlock";
@implementation AppDelegate (UMConfig)
- (void)configurateUMEvent{
NSDictionary *config = @{
@"TableViewController": @{
GLLoggingPageImpression: @"page imp - main page", //对TableViewController这个类的描述,页面统计用到
GLLoggingTrackedEvents: @[
@{
GLLoggingEventName: @"TableViewRow Click",
GLLoggingEventSelectorName: @"tableView:didSelectRowAtIndexPath:",
GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
//添加统计代码
NSLog(@"TableViewRow Click");
},
},
@{
GLLoggingEventName: @"renameFileName",
GLLoggingEventSelectorName: @"renameFile",
GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
//添加统计代码
NSLog(@"renameFileName");
},
},
],
},
@"DetailViewController": @{
GLLoggingPageImpression: @"page imp - detail page",
}
};
[self setupWithConfiguration:config];
}
typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);
- (void)setupWithConfiguration:(NSDictionary *)configs
{
// Hook Page Impression
[UIViewController aspect_hookSelector:@selector(viewDidAppear:)
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][GLLoggingPageImpression];
if (pageImp) {
NSLog(@"%@", pageImp);
}
});
} error:NULL];
// Hook Events
for (NSString *className in configs) {
Class clazz = NSClassFromString(className);
NSDictionary *config = configs[className];
if (config[GLLoggingTrackedEvents]) {
for (NSDictionary *event in config[GLLoggingTrackedEvents]) {
SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);
AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];
//AspectPositionAfter,在业务代码执行之后调用
[clazz aspect_hookSelector:selekor
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(aspectInfo);
});
} error:NULL];
}
}
}
}
反思
虽然实现了统计代码和业务代码的分离,但仔细想想,业务代码虽然可以肆无忌惮的删改,但是维护的统计config,也需要不断更新;如果每个开发人员都能做到在修改业务代码后都检查一下统计代码还好,可是如果统计代码是专人维护就糗大了,业务代码的变更维护人员根本不知道;所以,各种取舍吧