基于Aspects和JSPatch的热埋点方案 | luckymore的学习笔记
关于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完成热更新等等等等。大家都可以尝试下~