iOS 统计埋点方案探索解析与选择
前言
统计埋点,作为应用功能上线前的最后一环,对于一个应用的意义是尤为重要的。如果仅仅是去完成了一个项目而不知道这个项目的某个具体需求的使用率和用反馈率,这样显然不是我们想看到的。尤其是在互联网行业里,每年也许都有不同的风口,每个月也许都有不同的业务上线,如果没有统计埋点的数据化反馈,是无法理性的感知一个功能的完成是否能吸引用户或者解决用户的一个痛点。也只有量变才能慢慢的达到质变。
目前主流的统计平台多种多样,国外有谷歌分析(GA),国内有GrowingIO、友盟统计、腾讯移动统计等等,而每个统计都有自己的优缺点,但是万变不离其宗,根本统计方案都是大同小异的。对创业公司而言,每个公司的产品流动性不确定,产品岗的同学对于不同的统计平台了解程度又不同,所以经常会出现换统计平台的情况。项目刚启动的时候还好,但是如果项目已经很稳定而且相对庞大之后,这个工作量是恐怖的,你不会是想做,而是想死。最终的情况可能会是,技术不想动,产品要求换,这样就出现了多统计平台的尴尬界面,并不是根本的解决问题,而是应付彼此,坑害后面接手项目的人。如果你的项目已经是有了上面的情况,那么请你认真考虑和权衡一下是否需要重新审视一下自己项目的统计模块---这个项目的即是项目的结束,又是项目的开始的模块。
方案与选择
下面我们就来逐步分析一下目前iOS主流的埋点方案和各个方案的优缺点,每种方案都没有绝对的对错,这些方案都能完成产品的需求,但是如果我们在各自的团队内,有时间和有机会把埋点做的更好,那也是一件很酷的事情。我们可以根据自己目前的情况来选择最适合项目内使用的方案。
目前我找到的公开的最全面的埋点方案网易HubbleData无痕埋点SDK实现
把埋点划分为3大类
1.代码埋点
2.可视化埋点
3.无埋点
我们先来重新理解一下文中所提及的这三类埋点方式,因为第一次我在看的时候,也没有对后两种方式有特别清晰的理解。
1.代码埋点
对于第一类代码埋点,更准确的理解是纯手工埋点,因为不可能埋点不需要代码,即使是最后一种所谓的无埋点,也不可能做到不写代码就达到统计的效果。我们用GA举个例子,其他统计的原理一样,只不过是统计的代码有些不一样罢了。
对于页面统计:
//这是封装的统计管理类里的一个统计页面的方法
+(void)userPushToGAScreenName:(NSString *)GAScreenName
{
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value:GAScreenName];
[tracker send:[[GAIDictionaryBuilder createScreenView] build]];
}
因为早期我们在项目架构的时候,初始化了一个BaseController,在BaseController的viewWillAppear和viewWillDisappear里嵌入工具类的这个方法,就可以对GA发送一条统计消息。每个项目类名对应一个页面,通过映射关系来管理页面的统计,这个地方用了一个宏文件来保存记录的屏幕名称ScreenName,可能要使用大量的if()else()来做判断,这种方案的后续维护是反人类的,相对较好的方式如果不用这种大量判断的话,可以本地维护一个plist文件,不过后续的维护依旧繁琐,新来的小伙伴的操作成本是很高的。
针对事件统计:
+(void)GACreateEventWithCategory:(NSString *)category action:(NSString *)action label:(NSString *)label value:(NSNumber *)value withScreenName:(NSString *)screenName
{
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value:screenName];
[tracker send:[[GAIDictionaryBuilder createEventWithCategory:category action:action label:label value:value] build]];
[tracker set:kGAIScreenName value:nil];
}
对于事件统计,就需要我们深入到每个业务的内部去调用一下这个方法,来把事件的页面,方法名等参数传递进来,有时候还有一些用户的自定义操作,需要携带一些特殊的参数value进来。这个后期的维护工作简直是恐怖的,可能你写一个版本的统计之后,你也并不能验证这个统计是否就完全正确,交给产品去验收的话他们也不一定能测试出来是否起了效果。(特别是谷歌统计的原因,所有的统计都有滞后性,并不能立刻反馈,验收难度和时间成本都很高)。
缺点:
1.显而易见,你会在后期维护的时候写的怀疑人生
2.复用性差,几乎不能移植给其他项目
3.工作量大,而且会越写越多
4.统计代码上线之后,如果出现问题,只能后续版本迭代
5.如果统计项目名字改变了,原来老的APP版本依旧会统计老的页面名字
优点:
1.如果非要写一个其他统计无法做到的优点的话,那就是可自定义程度高吧,统计代码想写到那里写到那里(其实这些也可以在后面的方案实现,只是实现上稍微麻烦一点罢了)
2.最容易想到的方案(前期费时少,使用起来费手不费思路)
如果你受够了写上面的代码(不管你们受够了没有,反正我是受的够够的了),那么我们来分析下面的2种方案。
2.可视化埋点
“可视化埋点即用可视化交互的方式圈选出所要采集数据的控件,当用户行为产生时,即可收集到相应的埋点数据。相比于前面的代码埋点而言,可视化埋点能够解决代码埋点代价大成本高的问题,但是无法灵活的自定义埋点属性。”
可视化埋点上文中的定义很绕,我最后的理解是:动态下发后台配置的埋点数据,利用AOP的方式method swizzling需要统计的事件,添加统计代码进行统计,最后上传给后台统计结果。
这里我们要提一下AOP(切面编程思想),OC这边有一个很出名的开源库Aspects,利用Aspects,我们可以很方便的method swizzling拦截我们想要处理的方法,在执行对应方法前后插入我们的统计代码。具体的原理请参考微信读书团队Aspects的基本原理或者我写的这篇iOS 面向切面编程(AOP)开源框架Aspects源码解读
该方案的具体步骤就是:
(1)从后台获取需要统计的地方
(2)hook住需要统计的类的load方法来Method Swizzing要统计的方法
(3)上传统计到的事件给后台分析
我们用UIViewController,UITablview(collectionView与tableView基本相同),UIControl为例子,讲解一下该方案的思路。
1.UIViewController PV统计,页面的统计较为简单,利用Method Swizzing hook 系统的viewDidLoad, 直接通过页面名称即可锁定页面的展示代码如下:
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalDidLoadSelector = @selector(viewDidLoad);
SEL swizzingDidLoadSelector = @selector(analytic_viewDidLoad);
[MethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];
});
}
-(void)analytic_viewDidLoad {
[self analytic_viewDidLoad];
//用当前类的类名作为统计页面的标识符
NSString * identifier = [NSString stringWithFormat:@"%@", [self class]];
//通过当前类名获取PAGEPV表内的对应的页面的pageid和pagename
NSDictionary * dic = [[[AnalyticTool shareInstance].data objectForKey:@"PAGE"] objectForKey:identifier];
if (dic) {
NSString * pageid = dic[@"screenData"][@"pageid"];
NSString * pagename = dic[@"screenData"][@"pagename"];
[AnalyticTool upLoadScreenName:pagename withScreenID:pageid];
}
}
2.UIControl 点击统计,主要通过hook sendAction:to:forEvent: 来实现, 其唯一标识符我们用 targetname/selector/tag来标记,具体代码如下:
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzingSelector = @selector(analytic_sendAction:to:forEvent:);
[MethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
-(void)analytic_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
[self analytic_sendAction:action to:target forEvent:event];
NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [target class],
NSStringFromSelector(action),self.tag]; NSDictionary * dic = [[[AnalyticTool shareInstance].data objectForKey:@"ACTION"] objectForKey:identifier];
if (dic) {
NSString * eventid = dic[@"ActionData"][@"eventid"];
NSString * targetname = dic[@"ActionData"][@"target"];
NSString * pageid = dic[@"ActionData"][@"pageid"];
NSString * pagename = dic[@"ActionData"][@"pagename"];
[AnalyticTool upLoadActionEventWithScreenName:pagename withScreenID:pageid withTargetName:targetname withEventID:eventid];
}
}
3.TableView (CollectionView) 的代理统计
tablview的唯一标识, 我们使用 delegate.class/tableview.class/tableview.tag的组合来唯一锁定。 主要是通过hook setDelegate 方法, 在设置代理的时候再去hook住对应的代理方法来实现。因为tableview有多种代理方法,这里就不展开讲解了,具体方法跟上面的是类似的。就是多hook了一步。
缺点:
1.需要后台配合
2.可拓展性不是很高,因为需要修改后台下发的统计内容来每次的版本统计扩展
优点:
1.相对于第一种方案,代码量少了很多。
2.动态化从后台获取统计内容,方便线上修改
3.无埋点
其实无埋点才是真正的全埋点,本质上是利用runtime的方法也是利用AOP的思想,把所有的页面,事件,都给hook住然后自己定义一个层级概念,需要前段和后端协助开发一套完整的统计,把用户在应用内的全部操作都上传给服务器,然后服务器筛选出来需要的内容进行展示。这里需要再次提及一下上面的文章网易HubbleData无痕埋点SDK实现
这是详细讲解了无痕埋点的具体原理和难点,重点在于作者对普通view的层级结构path构造,非常巧妙,可以借鉴一下。
可视化埋点和无埋点最大的不同在于,前者是服务器定义统计的内容,后者是把全部内容统计然后提供给后台,后台可以自己选择需要的内容来使用。后者的可拓展性和动态性更高。
缺点:
1.对于小团队来说,几乎无法做到,因为需要权衡这个架构要求的时间
2.需要后台配合,前端业务量少了,但是后台的业务量没有减少,需要共同维护
优点:
1.前段代码可移植程度高
2.完全无侵入统计,业务代码不受影响。
3.前端统计非常安逸,几乎不需要做什么事情。
4.因为这种方式是全统计,所以不存在动态化了根本就,后面业务只要摘取需要的内容就可以,数据量充足。
写在最后
最好的方案永远是针对于不同的场景来说的,我们不可能在一个创业团队一开始就选择方案3的架构,所以对于你来说,你要自己抉择目前而言对你最好的方案,如果你没有后台业务同学的支持,方案1也许对你来说真的是最好的方案了,起码是可以完成统计需求,虽然苦点累点。但是在合适的时间,切换不同的选择,才是成长的体现,还是最开始的话,如果你在的团队,已经给你了资源和时间去完善埋点这个模块,如果你把它做的更好,那一定是一件很酷的事情。
参考资料
1.网易HubbleData无痕埋点SDK实现
2.iOS无埋点数据SDK实践之路
3.可视化埋点方案
4.美团前端无痕埋点方案
5.iOS打点杂谈
6.iOS动态性可复用而且高度解耦的用户统计埋点实现
7.微信读书团队Aspects的基本原理
8.iOS 面向切面编程(AOP)开源框架Aspects源码解读