代码埋点
代码埋点是一种常规且直观的方案,需要开发人员在需要埋点的页面或点击事件的响应方法中注入埋点统计相关方法进行数据统计上报。也可以接入三方统计分析SDK来实现,比如友盟UMCAnalytics
统计分析库。实现方式如下代码所示:
// 通过在HomeViewController 中的“页面已显示”和“页面已消失”两个回调中注入统计代码实现埋点
@implementation HomeViewController
//...other methods
- (void)viewDidAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];
}
@end
这种埋点方式虽然实现起来简单,但是有很多缺点造成无法在实际项目中使用,如果需要埋点的页面很多,会增大工作量,代码侵入性太强,且不易于后期维护。
无埋点(无痕埋点)
通过Runtime的Method Swizzling 特性使用 AOP面向切面编程的思想 hook住系统关键方法,注入埋点代理实现统计上报功能。这种方式对项目没有侵入性,可以对某个控件进行全局埋点,使用更灵活。
比如我们要对某个页面的打开次数做统计,那么可以对UIViewController
添加分类UIViewController+userStastistics
来对控制器的viewWillAppear:
方法进行hook,注入次数统计代码,代码示例如下:
// UIViewController 的分类
@implementation UIViewController (userStastistics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(swiz_viewWillAppear:);
// 使用hook工具(内部使用Method swizzing技术实现)交换两个方法的实现(IMP)
[HookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];
});
}
#pragma mark - Method Swizzling
- (void)swiz_viewWillAppear:(BOOL)animated
{
// 注意:此行代码的调用实质上执行的是viewWillAppear: 的原生实现
[self swiz_viewWillAppear:animated];
// 添加埋点统计代码,用于记录页面打开次数
Add your custom Analytics code.
}
@end
上述代码就可以实现无侵入性的数据埋点,但是这种方法会对所有UIViewController
及其子类的viewWillAppear:
方法进行hook代码注入,也就是说所有页面都会被统计进去,为了实现部分页面的统计,可以进行过滤,或使用白名单。
事件唯一ID的确定
在对事件进行上报的时候,需要对事件进行标记,我们可以创建一个plist配置表对每个事件定义一个唯一标识符、事件id、相关参数等信息,然后在埋点响应方法中通过获取事件发生的类名或方法名来确定事件,并上传到统计服务器。
AOP编程
Aspect Oriented Programming (AOP)是面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。实现方式如下图所示:
AOP的主要功能是:日志记录、性能统计、安全控制、事务处理、异常处理等。
主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望将它们独立到非指导业务逻辑的方法中,进而改 变这些行为的时候不影响业务逻辑的代码。
可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等),是OOP的一个有效补充。
在iOS开发中,Aspects三方库就是基于AOP的思想来实现的,内部使用了ObjC动态特性Method swizzling Hook住原生方法注入block中的自定义代码。通过使用这个库,可以简单高效的实现无埋点数据统计。
无埋点采集
下面我们将介绍几种常用的UI控件的事件采集方式,分别可以这样实现:
UIViewController
主要是收集页面的生命周期,采用hook UIViewController的viewWillAppear:
方法,将埋点代码插入到被替换的xxx_viewWillAppear:
方法中,实现采集。
[HookUtility swizzlingInClass:[self class] originalSelector:@selector(viewWillAppear:)swizzledSelector:@selector(xxx_viewWillAppear:)];
UIControl
针对UIControl,HubbleData采用的是hook UIControl的sendAction:to:forEvent:
方法。由官方文档可知,在UIControl执行对应的action时都会首先调用sendAction:to:forEvent:
方法,HubbleData的实现如下:
[DASwizzler swizzleSendActionSelector:@selector(sendAction:to:forEvent:)
onClass:[UIControl class]
withBlock:executeBlock];
考虑到UIControl的子类较多,所以HubbleData选取了其中使用较多的几种进行了特殊的分析:主要是UITextField、UIButton和UISwitch,其余的暂时未做特殊分析。具体的埋点的采集设计为:无论是哪种UIControl,EventID均采用的是第三部分介绍的唯一标识字符串的SHA256编码值,但是相关采集properties有所差别。
UITextField
UITextField是UIControl的一个子类,由于UITextField涉及到用户的隐私比较多,比如用户名、密码、聊天文本等,所以HubbleData不会对此类的UITextField进行埋点的采集。
HubbleData主要采集的是UISearchBar中的UITextField,即UISearchBarTextField,并获取搜索的文本内容,这对于一些电商类的App来说,能够较好的分析用户感兴趣的商品等,这是作为HubbleData SDK无埋点的一个需求。
hook住sendAction:to:forEvent:
后,如果对UISearchBarTextField的所有actions都进行hook的话,那么_searchFieldBeginEditing、_searchFieldEndEditing等所有的action发生的时候都会进行数据的采集,会采集到很多无用的信息,导致采集的数据混乱。HubbleData SDK只有当_searchFieldEndEditing action发生时才会进行埋点,收集的properties为:
(1) type 为UIControl采集的事件类型,这里设置为searchBarEvent;
(2) page 为当前页面的名称,用于前端显示用;
(3) searchText 为_searchFieldEndEditing发生时采集到搜索框的搜索文字(此字段不为空);
这样就能对搜索框进行无埋点采集,并能收集搜索的文本内容。此方法只是在_searchFieldEndEditing发生时采集数据,有可能该action执行时并未尽兴真正的搜索操作,可能会与业务数据库的数据有出入,但是也能够较为准确的分析用户感兴趣的搜索内容。
UIButton
UIButton是最常见的一种UIControl,由于UIButton在使用过程中会有多种状态,所有在记录的时候需要上报更详细的参数,可以添加titleColor
、imageName
、frame
等属性来做具体区分。
UISwitch
类似于UIButton,只不过这里要采集switchState,即当前的开关状态,具体的采集属性为:
(1) type 为UIControl采集的事件类型,这里设置为switchEvent;
(2) page 为当前页面的名称,用于前端显示用;
(3) switchState 为switch的开关状态;
UITableView和UICollectionView
针对UITableView和UICollectionView,HubbleData采用的是先hook UITableView和UICoolectionView的setDelegate:
方法,然后找到对应的delegate,然后再hook delegate类中的tableView:didSelectRowAtIndexPath:
方法和UICollectionView的collectionView:didSelectItemAtIndexPath:
方法。这里以UITableView为例:
//先hook setDelegate:方法
[DASwizzler swizzleSelector:@selector(setDelegate:)
onClass:[UITableView class]
withBlock:executeSetDelegateBlock];
//再hook delegate的tableView:didSelectRowAtIndexPath:方法
void (^executeSetDelegateBlock)(id, SEL, id) = ^(id view, SEL command, id<UITableViewDelegate> delegate) {
if ([delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
[DASwizzler swizzleSelector:@selector(tableView:didSelectRowAtIndexPath:)
onClass:[delegate class]
withBlock:executeBlock];
}
};
// "executeBlock" block 中插入埋点代码
EventID按照上述介绍的方法获取,只不过这里要注意的是,获取的并不是UITableView的唯一标识字符串而是对应的点击的cell的唯一标识字符串。采集的properties为:
(1) type 为UITableView采集的事件类型,这里设置为tableViewSelectEvent;
(2) page 为当前页面的名称,用于前端显示用;
(3) section 为点击的cell所在的section;
(4) row 为点击的cell所在的row;
UIGestureRecognizer
在iOS开发中,经常会使用一些手势来处理一些点击的操作,所以也有必要对UIGestureRecognizer进行hook。HubbleData 并不是直接针对UIGestureRecognizer这个类进行hook,而是hook UIView类的addGestureRecognizer:
方法,实现如下:
// hook addGestureRecognizer: 方法
[DASwizzler swizzleSelector:@selector(addGestureRecognizer:)
onClass:[UIView class]
withBlock:executeBlock];
// 执行block 判断手势类型,并插入响应埋点代码
void (^executeBlock)(id, SEL, id) = ^(id target, SEL command, id arg) {
if ([arg isKindOfClass:[UITapGestureRecognizer class]] ||
[arg isKindOfClass:[UILongPressGestureRecognizer class]]) {
[arg addTarget:self action:@selector(da_autoEventAction:)];
//在本类下添加一个action的实现
...........
}
};
通过hook addGestureRecognizer:
方法,可以得到该UIView所添加的UIGestureRecognizer,这里只对UITapGestureRecognizer和UILongPressGestureRecognizer进行处理,其他的手势暂未做处理。得到相应的UIGestureRecognizer,添加一个action,当该手势执行的时候,同样会执行该action,在action中执行埋点的操作。
这里获取的是UIGestureRecognizer所在的UIView的唯一标识标识字符串编码作为EventID,采集的属性为:
(1) type 为UIGestureRecognizer采集的事件类型,这里设置为gestureTapEvent;
(2) page 为当前页面的名称,用于前端显示用;