用户统计知识:
用户行为统计一直是移动互联网产品中必不可少的环节,也俗称埋点。在保证移动端流量不会受较大影响的前提下,PM们总是希望埋点覆盖面越广越好。目前常规的做法是将埋点代码封装成工具类,但凡工程中需要埋点(如点击事件、页面跳转)的地方都插入埋点代码。一旦项目越来越复杂,你会发现埋点的代码散落在程序的各个角落,不利于维护以及复用。本文利用iOS的运行时机制实现一种可复、解耦、容易维护的用户统计方案。
该方案的完成将会用到以下知识:
运行时互换函数调用顺序, 类方法:
method_exchangeImplementations
一、常规埋点做法
接着开头的话题,我们先回顾一下主流的埋点是怎么做的。我粗糙地将埋点分为两种:1、页面统计,包括页面停留时间、页面进入次数;2、交互事件统计,包括单击、双击、手势交互等。
1)常规页面统计埋点
以统计页面进入次数为例,最简单粗暴的做法是在所有页面的viewDidAppear:以及viewDidDisappear:中分别埋点,将自己对应的pageID上传给服务端。代码大概长酱紫:
- (void)viewDidAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[UMSAgent startTracePage:pageName];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[UMSAgent endTracePage:pageName];
}
+[UMSAgent postEvent:(NSString *)event_id]封装网络请求,将ID上传给服务器。
上述方案有以下弊端: |
---|
1、复用性差。这部分埋点代码很难给其他项目复用 |
2、工作量大。尤其当页面较多时,需要修改的代码较多 |
3、引入“脏代码”,不易维护 |
第3点提到的“脏代码”意思是用户行为分析这种业务其实跟主业务没太大关系,不应该保持如此高的耦合度,因为这些代码会干扰我们对项目主业务的维护。这个我个人看法。
2)常规交互事件埋点
常规做法一般在交互事件的selector中获取该事件的ID并上传给服务端,代码大概长酱紫:
- (IBAction)onFavBtnPressed:(id)sender
{
[UMSAgent postEventJSON:事件ID json:其他事件];
}
稍微大一点的APP如果采用这种方式,那诸如此类的埋点代码将遍地都是。它的缺点参考页面统计埋点部分,其复用性基本为零,也就是在新项目中根本无法复用埋点代码
小总结一下,采用常规的做法虽然直观方便,但在可复用性、可维护性等方面有所欠缺。在我看来,借助运行时可以很好地避开这些缺点。 |
---|
二、Method Swizzling、Hook与代码注入
由于Runtime知识不属于本文的重点,这里只简单介绍。
在iOS中,我们可以在运行时替换两个方法的实现,达到“勾住”某个方法并注入代码的目的。具体做法是:
重载类的“+(void)load”方法,在程序加载到内存时利用Runtime的method_exchangeImplementations等接口将方法(设为M)的实现互相交换。当方法M被调用时就会被勾住(Hook),执行我们的方法。
这种技术也称为Method Swizzling,属于面向切面编程(Aspect-Oriented Programming)的一种实现。
*
实例方法:
运行时互换函数调用顺序, 实例方法
static void transforInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
if (originalMethod != NULL && swizzledMethod != NULL)
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
类方法
/**
* 运行时互换函数调用顺序, 类方法
*/
static void transforClassMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
if (originalMethod != NULL && swizzledMethod != NULL)
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
更多关于Runtime、method swizzling、面向切面编程的介绍请参考这里
三、基于运行时的埋点方案
为了便于下文叙述,先引入一个简单的项目,共有两个页面(ViewController,DetailViewController),如下:
-(void)transfor_viewWillAppear:(BOOL)animated
{
//插入需要统计的代码
[self transfor_viewWillAppear:YES];
[CollectManager startTracePage:NSStringFromClass([self class])];
}
-(void)transfor_viewWillDisappear:(BOOL)animated
{
[self transfor_viewWillDisappear:YES];
[CollectManager endTracPage:NSStringFromClass([self class])];
}
+ (void)load
{
//上传客户唯一标识
runTimeUtil.transforInstanceMethod([self class],
@selector(viewWillAppear:),
@selector(transfor_viewWillAppear:));
runTimeUtil.transforInstanceMethod([self class],
@selector(viewWillDisappear:),
@selector(transfor_viewWillDisappear:));
}
需求是
统计两个页面的展示与离开次数
统计收藏、分享单击事件的次数
对现有工程代码影响越小越好
1)统计两个页面的展示与离开次数
这部分应该比较直观了,摒弃掉在每个controller中埋点的方式,我们对UIViewController添加category从而Hook到viewWillAppear:与viewWillDisappear:。在这两个方法中注入埋点代码:
到这里先做了阶段性的总结,本文提出的思路有以下优越性:
- 与工程代码基本解耦,避免引入“脏代码”
- 维护配置表比维护散落在工程各个角落的代码简单
四、基于单元测试的后期维护
俗话说,创业难守业更难。前面的思路基本可以完成初步的埋点需求。但是在实际项目中代码重构是很频繁的。这意味着在多人协作开发、代码重构频繁的项目中响应事件方法甚至页面名称都可能被改掉,造成事件ID获取不到导致埋点失效。
代码变动的情况无非以下几种(这里只介绍响应事件发生改变的情况):
建议事件ID 统计有一个专门的配置表比如(Config)
1、响应事件方法名称改变或者删除
比如有的同事,忘记在ViewController 实现 button 的 方法(collectWithEventId:) 这种疏忽在开发压力大进度赶的情况下是有很大概率发生的。由于代码与配置表不匹配将导致eventID为nil。在这种情况下单元测试就很有必要了,使用完备的测试用例能在发版前检测到这种不匹配情况从而避免埋点失效。
五、结语
以上就是结合运行时所设计出的用户统计思路全部内容。应该说该方案的可复用性与解耦程度都是不错的,既适合于新建的工程,也适合于已经创建的工程。看起来内容多,其实总结起来无非几个步骤:配置表+Hook+单元测试。利用Method Swizzling把埋点代码集中管理其实也是合理的,有利于专人开发、跟踪及维护。当然以上思路只考虑简单的情形,更复杂的情况就需要变通了,但总体思路就是如此。
思路可能不完美,但作为一种尝试也未尝不可。路都是走出来的。