基于Aspects和JSPatch的热埋点方案

关于Aspects

https://github.com/steipete/Aspects

aspects是针对面向切面编程:Aspect Oriented Programming(AOP)的一种实现方案。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
以埋点为例,虽然现在已经有像友盟这样强大的第三方埋点方案,但是这并不能满足一些公司的业务需求,如果要完全自定义地进行埋点,监听并统计用户的行为,使用传统的方案,必然导致对整个项目的代码进行大范围的修改。而使用面向切面编程的思想,则可以将埋点和系统原有的逻辑解耦,悄悄地完成埋点。


下面来看看Aspects是如何帮助我们实现这点的。

还是举个例子来说明:
假设我们要统计某些页面的显示的次数,以往使用友盟来统计页面访问我们会这么做

1
2
3
4
5
6
7
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//do something
if(self.title){
[MobClick beginLogPageView:self.title];
}
}

聪明一点的做法当然是可以把它写在基类。这样至少不会影响太多的代码。但是如果是其他一些更加个性化的埋点,比如说点击某个按钮,通过不同的方法进入某个页面,注册所消耗的时间统计等等等等。。。。

有了Aspect 我们可以这么做

1
2
3
4
5
6
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
UIViewController* viewController = aspectInfo.instance;
if (viewController.title) {
[MobClick beginLogPageView:viewController.title];
}
} error:NULL];

当然,这块代码完全可以和原有的逻辑隔离。
先解释一下,Aspects针对NSObject实现了aspect_hookSelector的方法,顾名思义,使用一个钩hook住了UIViewController的viewWillAppear方法,withOptions可以有两个参数,一个是AspectPositionAfter,另一个是AspectPositionBefore,分别代表在原有方法执行前或者原有方法执行后执行该block,根本上相当于利用oc的runtime特性替换了原有的方法。
通过aspectInfo中的instance和arguments属性可以分别获得方法的方法体和参数,基本上也就是获得了整个方法执行的上下文。
特别需要注意的是,同一个继承树上的同一个方法只能被hook一次

下面是使用Aspect的完整的埋点策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)configFromArray {
//一个完成的用于配置的NSArray
//NSArray *configArray = [HWTracerInfo configArray];
NSArray *configArray = @[
@[NSStringFromClass([HWMainViewController class]),@"cgButtonPressed",@"Game_Button_Pressed"],
@[NSStringFromClass([SetNicknameViewController class]),@"initWithUserInfo:gender:",@"Register_Name_Length",(NSArray*)^(id instance, NSArray* arguments){
NSString* nickname = [arguments firstObject];
return @[@(nickname.length), @"Register_Name_Length"];
}]
];
for (NSArray *item in configArray) {
//第一个元素是类名
NSString *className = item[0];
//第二个元素是方法名
NSString *selectorName = item[1];
//第三个元素代表埋点类型的标识
NSString *actionName = item[2];
Class targetClass = NSClassFromString(className);
SEL targetSelector = NSSelectorFromString(selectorName);
NSError *error;
//根据以上元素使用Aspects
[targetClass aspect_hookSelector:targetSelector withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
int value = 1;
//如果包含第四个元素,即block,则传入instance和arguments参数,执行block,根据返回的参数来进行埋点
if (item.count > 3) {
HWTracerBlock block = (HWTracerBlock)item[3];
NSArray* result = block (aspectInfo.instance,aspectInfo.arguments);
//根据结果埋点
} else {
//直接埋点
}
} error:&error];
if (error) {
//输出错误
}
}
}

我们可以把需要的埋点全部用NSArray的形式配置在一个列表中,通过提供类名、方法名、标识、以及必要时的block来统一的配置埋点,一旦触发了某次操作就进行一次记录。

可以看出,对于一些情况的埋点,比如只要执行了某个方法就做一次记录,完全可以单纯地用一系列的字符串就可以做到,可以写到配置文件中或者远程部署,但是对于一些相对复杂的埋点,需要通过方法执行的上下文来判断,就必须传入block,因而想要做到远程控制埋点很困难。

然后JSPatch的出现让远程控制所有的埋点成为了可能

关于JSPatch

关于JSPatch 可以参见作者bang的博客
http://blog.cnbang.net/works/2767/
本人才疏学浅,并没有弄懂其实现原来,但是先尝试着用了起来。
JSPatch主要是为了动态更新iOS,当然也可以通过它来动态实现热部署

之前说到,想要远程控制所有的埋点瓶颈在于block
而JSPatch可以通过javascript脚本来实现block,自然也就解决了这个问题。

方案也很简单,原先的埋点方案是
item[0] = 类名
item[1] = 方法名
item[2] = 埋点标识
item[3] = block (非必须)
此时可以将item[3]替换成一个bool值或者一个标识
然后在解析的时候判断是否需要block

如果需要就调用

1
[JPEngine evaluateScript:script];

执行脚本,脚本的内容当然就是block的内容。
需要注意的是,JSPatch中执行Javascript中定义的block需要注意,参数由OC传入javascript采用以下方式:

1
2
3
- (void)request:(void(^)(NSString *action, NSArray arguments))callback {
callback(self.action, self.arguments);
}

具体可以参看( https://github.com/bang590/JSPatch )中对block使用的介绍

总结

使用Aspects我们可以实现将埋点和原有代码完全解耦
使用JSPatch和Aspects结合我们可以实现埋点的远程部署。
当然这两者在别的方面还有很好的应用。比如使用Aspects来代替项目中的基类,使用JSPatch完成热更新等等等等。大家都可以尝试下~