移动开发经过前几年爆炸式的增长之后,移动开发进入了一个精细化管理操作的时期,除了拓展自己的能力,在移动开发外寻求到更好的职业道路这条路外,移动开发这份工作的做法怕也是需要有一番观念上的变动。
对于应用内的定向、精细化管理,其中重要一点就是App内数据采集。
数据采集可分为 埋点数据采集和无埋点数据采集,从名字字面可以分出二者的区别,埋点数据采集目前国内的主要第三方数据分析服务商,如百度统计、友盟、TalkingData 等都提供了这一方案,以友盟提供SDK为例
//自动页面时长统计, 开始记录某个页面展示时长.
+ (void)beginLogPageView:(NSString *)pageName;
//自动页面时长统计, 结束记录某个页面展示时长.
+ (void)endLogPageView:(NSString *)pageName;
//结构化事件
+ (void)event:(NSArray *)keyPath value:(int)value label:(NSString *)label;
在需要进行统计的页面上进行数据采集埋点,调用SDK对应提供的API,完成数据统计,埋点数据采集的好处是在于:调用直观简单、API对开发者友好、另外由于这些大厂商进入该领域很早,已经占领了很大的移动开发市场有了很健壮便捷的服务提供、同时提供了非常丰富的各种表项。
不好的地方在于:
在介绍无埋点数据采集之前,首先需要将Objective-C 作为动态语言的特性进行讲解,这是实现无埋点数据采集的基石。
Objective-C作为动态语言,Class在是runtime中创建的, selector, method, imp, protocol等都是随后绑定上去的,这是所谓的运行时绑定。一个典型的例子如下
首先在runtime中能够查出当前运行时环境中所有的类,每个类中的方法,每个类消息的绑定,每个类的实现的协议,每个协议的定义,每个类当前的消息缓存等一切你想知道的东西。
其次runtime底层Class实现是对消息转发实现的,Class的方法调用都是间接的。
基于以上两个特性促成了Objective-C的黑魔法 Method Swizzling,Method Swizzling的实现逻辑如下图所示。
新建一个数据节点采集的类,交换被采集的类和 采集节点的 对应两个selector对应的IMP。在采集节点中对数据进行收集。
在以上基础上,对Objective-C中的类接口进行对应数据的采集。
iOS提供的基础类的selecter都是已知的,因此较为简单,只需要通过在对应类调用之前进行对应接口的hook,在hook方法中进行相应的数据采集即可。
下面的例子以对UIViewController
为例,在每个页面中采集用户在一个页面内停留的时间,实现代码如下:
在声明的对进行数据采集分析的类 CCAnalyseViewControllerAnalyseNode
的load
方法中加入对UIViewController
两个方法和本类的方法的IMP进行交换。
- (void)viewDidAppear:(BOOL)animated;
- (void)viewDidDisappear:(BOOL)animated;
将 CCAnalyseViewControllerAnalyseNode
实例化之后,每个UIViewController
在加载、退出的时候调用上面两个方法时,执行顺序如下图所示:
此时就能够在UIViewController
对应2个方法中都加入了可以用来相应数据采集的方法,就可以在对应的hook方法中进行页面停留时间的采集。
思路同上,如果需要对用户操作习惯和常用功能的进行统计,可以对UIControl
方法进行hook实现。
除此之外,对于 UITableView
、UIViewController
常用的控件,可以通过hook 对应的 delegate 方法,获取在实际使用中业务赋值给对应cell的数据、或者cell点击事件。
以上两个例子是对系统提供的类进行数据采集的例子。
正如上一节开头所言,由于系统提供类selector都是已知的,所以实现起来较为简单,而对于用户自定义类在selector未知的情况下,如何进行hook?
首先在采集节点中植入一个能够对所有 Class 进行解析的 接口,然后通过线上进行部署,其次将Class的ClassName、seletor参数下发给数据采集SDK,实现对用户自定义Class的数据采集。
- (void) analyseUserdefinedTarget:(NSString *)targetClass action:(nullable SEL)action method:(IMP)method;
该接口的具体实现为:
- (void) analyseUserdefinedTarget:(NSString *)targetClass action:(nullable SEL)action method:(IMP)method{
if (targetClass !=nil && action !=nil) {
Class target = NSClassFromString(targetClass);
NSObject *temp = [[target alloc]init];
if ([temp respondsToSelector:action]) {
NSString * methodName = NSStringFromSelector(action);
Method m_gesture = class_getInstanceMethod([target class], action);
SEL hook_sel = NSSelectorFromString([NSString stringWithFormat:@"hook_%@",methodName]);
if (method == nil) {
method = (IMP) myHookMethodIMP;
}
if (![[CCAnalyseBasicAnalyseNode sharedInstance] respondsToSelector:hook_sel]) {
class_addMethod([CCAnalyseBasicAnalyseNode class], hook_sel, method, method_getTypeEncoding(m_gesture));
class_addMethod([target class], hook_sel, method_getImplementation(m_gesture), method_getTypeEncoding(m_gesture));
}
method_setImplementation(m_gesture, class_getMethodImplementation([CCAnalyseBasicAnalyseNode class], hook_sel));
}
}
}
以上代码的逻辑是:对用户自定义Class进行selector检查,如果该类的确有对应接口,才需要对爱selector进行hook,避免对未声明的selector进行处理出现崩溃。在采集节点中去检查添加的hook接口是否存在,不存的话,在runtime中添加selector、和对应的IMP;最后将被采集用户自定义类的selector指向的IMP与新添加的IMP进行交换。在myHookMethodIMP中完成对该接口的数据采集分析。
2. IMP的实现、以及IMP中数据采集,以上文中 myHookMethodIMP为例,看IMP的具体实现
void myHookMethodIMP(id self, SEL _cmd,id arg0)
{
if ([self respondsToSelector:_cmd]) {
NSString * methodName = NSStringFromSelector(_cmd);
SEL hook_sel = NSSelectorFromString([NSString stringWithFormat:@"hook_%@",methodName]);
controlInfo = [NSMutableDictionary new];
[controlInfo setObject:[NSString stringWithUTF8String:object_getClassName(self)] forKey:ACTIONTARGET];
[controlInfo setObject:methodName forKey:ACTIONNAME];
[controlInfo setObject:[NSNumber numberWithInteger:ActionTypeUserdefined] forKey:ACTIONTYPE];
if (arg0 !=nil) {
if ([arg0 isKindOfClass:[NSObject class]] ) {
if ([arg0 isKindOfClass:[NSString class]]||[arg0 isKindOfClass:[NSNumber class]]||[arg0 isKindOfClass:[NSArray class]]||[arg0 isKindOfClass:[NSDictionary class]]||[arg0 isKindOfClass:[NSNull class]]) {
[controlInfo setObject:arg0 forKey:ACTIONINFO];
}
}else{
[controlInfo setObject:[NSValue valueWithNonretainedObject:arg0] forKey:ACTIONINFO];
}
}
NSDictionary *reportInfo = @{[NSNumber numberWithInteger:[self hash]]:controlInfo};
addActionReport(reportInfo);
if ([self respondsToSelector:hook_sel]) {
IMP imp = [self methodForSelector:hook_sel];
void (*func)(id, SEL,id) = (void *)imp;
if (imp!=NULL) {
func(self, hook_sel,arg0);
}
}
}
}
在IMP中,采集到该selector接口的参数之外,仍然需要调用原类中IMP,实现原来Class的方法。
iOS 如何实现可变参数的IMP,实现对用户自定义类hook
3. 线上下发需要被采集Class的相关参数;
举如下例子,采集UILabel的 text,使用该节点采集接口使用方式如下
[[CCAnalyseManager sharedInstance] analyseUserdefinedTarget:@"UILabel" action:@selector(setText:)];
实际使用中,通过线上管理系统向数据采集SDK,下发指令数据,SDK内完成对JSON的解析,即可以实现线上实时管理需要采集的数据。
{
"className": "UILabel",
"selectorName": "setText"
}
综上所述,使用无埋点数据采集方案可以实现埋点数据采集方案不足的地方:
1.实现自动收集基本的页面和事件信息,分离数据采集与业务代码;
2.数据采集类型与需求根据线上配置设置与操控,无需再耗费大量的时间和精力对需要统计的页面和事件挨个单独埋点。
3.线上管理配置、增加数据采集需求时,也无需再次发版。不再为少埋点、漏埋点而发愁。