iOS|iOS UI控件埋点技术方案之基于runtime hook
关于大数据,记得马爸爸说过一句话,具体哪几个字忘了,但大概的意思是:未来数据就是最大的财富。互联网发展到今天,特别是移动互联网,数据就是财富已经开始在被验证,各个互联网公司都在从各个方面收集提炼数据,分析数据,然后变成财富。^_^
说了这么多,还没切入正题,好吧,对于我们iOS客户端,也有大量的数据需要收集,通过统计客户对于我们app的使用行为,不断的改进我们的app,通过分析趋势,拓展公司的盈利模式,改变公司经营战略都有可能。所有说这么多,就是想说明收集用户行为数据对于我们app来说也是很重要的。
那么,怎样才能进行数据收集?比如,用户在界面点击了某个按钮,很容易想到的是打印日志文件,一个按钮可以,一个app上存在成千上百个按钮,手势,界面,都打印日志,这样导致日志文件有很多,考虑后续我们从日志文件提炼用户行为的数据比较庞大复杂,既无形的增加了开发的工作量,也增加了数据分析的工作量。所以说最好的办法还是“专人专事”,即:app中有个模块,再不影响其他业务逻辑的情况下,单独负责数据统计收集,这就是埋点技术。
一、埋点控件
哪些控件需要埋点呢,根据用户与app的交互方式包括点击按钮(UIControl)、手势(UIGestureRecognizer)、列表某一行的点击(UITableView)、查看了某个界面(UIViewController)等,以及公司的业务需求(可以用配置文件的方式)。本文具体讲交互方式,即UI控件交互方式的捕捉。
二、埋点的技术架构
文章图片
埋点架构 三、交互UI的事件捕捉
1、原理
利用runtime运行时机制,将类原生方法替换成用户自定义的方法,相当于强行在原本调用栈中插入一个方法,我们在其中插入一段统计代码即可,需要注意的是不要多次替换,谨防其他代码重复替换。
1.1、黑魔法原理
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
文章图片
交换原理 1.2、黑魔法用法
先给要替换的方法的类添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。
由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。
2、UI埋点实现技术方案
2.1、类图
文章图片
实现类图 【iOS|iOS UI控件埋点技术方案之基于runtime hook】2.2、UI埋点具体实现
2.2.1、SwizzManager实现(hook工具类)
/** *方法交换 *@param clazz 交换的类 *@param originSel 需要交换的原始方法sel *@param newSel 动态方法sel */2.2.2、UIControl (Analysis)
+ (void)swizzMethodForClass:(Class)clazz originSel:(SEL)originSel newSel:(SEL)newSel{
swizzleMethod(clazz, originSel, newSel);
}
/** *动态添加方法并交换 *@param clazz 交换的类 *@param impClass 动态方法的imp所在类 *@param impSel 动态方法的imp对应的sel *@param originSel 需要交换的原始方法sel *@param newSel 动态方法sel */
+ (void)swizzMethodForClass:(Class)clazz newSelImpClass:(Class)impClass impSel:(SEL)impSel originSel:(SEL)originSel newSel:(SEL)newSel{
IMP newImp = method_getImplementation(class_getInstanceMethod(impClass, impSel));
BOOLresult =class_addMethod(clazz, newSel, newImp,nil);
result = result && [selfcontainsSel:originSelinClass:clazz];
if(result) {
swizzleMethod(clazz, originSel, newSel);
}
}
///类是否包含方法
+ (BOOL)containsSel:(SEL)sel inClass:(Class)class{
unsignedintcount;
Method*methodList =class_copyMethodList(class,&count);
for(inti =0; i < count; i++) {
Methodmethod = methodList[i];
NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
if([tempMethodStringisEqualToString:NSStringFromSelector(sel)]) {
returnYES;
}
}
return NO;
}
///交换方法
voidswizzleMethod(Classclass,SELoriginalSelector,SELswizzledSelector)
{
// the method might not exist in the class, but in its superclass
MethodoriginalMethod =class_getInstanceMethod(class, originalSelector);
MethodswizzledMethod =class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOLdidAddMethod =class_addMethod(class, originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if(didAddMethod) {
class_replaceMethod(class, swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
}
else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
当操作事件发生时,底层主动调用该方法sendAction:to:forEvent:来触发action,因此通过hook该方法,就可以拿到用户的UI事件,例如UIButton的点击事件,具体实现如下:
+(void)load{2.2.3、UIGestureRecognizer (Analysis)
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
Classclazz = [selfclass];
SELoriginalSelector =@selector(sendAction:to:forEvent:);
SELnewSelector =@selector(hxw_sendAction:to:forEvent:);
[SwizzManagerswizzMethodForClass:clazzoriginSel:originalSelectornewSel:newSelector];
});
}
///自定义发送点击响应方法
-(void)hxw_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event{
[selfhxw_sendAction:actionto:targetforEvent:event];
[__analysisDelegateUIControl hxw_UIControlSendAction:action target:target forEvent:event];
}
我们在初始化手势的时候,会给手势添加响应事件,但是手势不像UIControl那样,暴漏了相应事件主动调用的action的方法,但是没关系,我们知道绑定action的方法,就是通过hook绑定事件的方法,拿到相应的action和target,然后hook住target的action方法(先给target添加一个方法,然后与action交换),在hook的方法中就可以得到事件的响应时机,具体实现如下:
+(void)load{2.2.4、UIViewController (Analysis)
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
SELoriginSel =@selector(initWithTarget:action:);
SELnewSel =@selector(hxw_initWithTarget:action:);
[SwizzManagerswizzMethodForClass:[selfclass]originSel:originSelnewSel:newSel];
SELoriginSel1 =@selector(addTarget:action:);
SELnewSel1 =@selector(hxw_addTarget:action:);
[SwizzManagerswizzMethodForClass:[selfclass]originSel:originSel1newSel:newSel1];
});
}
-(instancetype)hxw_initWithTarget:(id)target action:(SEL)action{
UIGestureRecognizer* gestureRecognize = [selfhxw_initWithTarget:targetaction:action];
if(!target && !action) {
returngestureRecognize;
}
if([targetisKindOfClass:[UIScrollViewclass]]) {
returngestureRecognize;
}
[selfhandleTarget:targetaction:action];
returngestureRecognize;
}
-(void)hxw_addTarget:(id)target action:(SEL)action{
[selfhxw_addTarget:targetaction:action];
[selfhandleTarget:targetaction:action];
}
- (void)handleTarget:(id)target action:(SEL)action{
Classclazz = [targetclass];
NSString* newMethodName = [NSString stringWithFormat:@"hxw_%@_%@",NSStringFromClass(clazz),NSStringFromSelector(action)];
SELnewSel =NSSelectorFromString(newMethodName);
SELimpSel =@selector(respondActionForGestureRecognize:);
// 向类身上添加方法并交换
[SwizzManager swizzMethodForClass:clazz newSelImpClass:[self class] impSel:impSel originSel:action newSel:newSel];
self.name= newMethodName;
}
- (void)respondActionForGestureRecognize:(UIGestureRecognizer*)gestureRecognize{
///调用原始action,self为target
NSString* identifier = gestureRecognize.name;
SELsel =NSSelectorFromString(identifier);
if ([self respondsToSelector:sel]) {
IMPimp = [selfmethodForSelector:sel];
void(*func)(id,SEL,id) = (void*)imp;
func(self,sel,gestureRecognize);
}
[__analysisDelegateUIGesture hxw_UIGestureCognizedRespondAction:gestureRecognize];
}
UIViewController这个简单,只需要hook住UIViewController的时机viewDidLoad、viewWillAppear、viewDidDisappear方法就可以完成页面的统计
+ (void)load{2.2.5、UITableView (Analysis)
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
SELoriginSel =@selector(viewDidLoad);
SELnewSel =@selector(hxw_viewDidLoad);
[SwizzManagerswizzMethodForClass:[selfclass]originSel:originSelnewSel:newSel];
SELoriginSel1 =@selector(viewWillAppear:);
SELnewSel1 =@selector(hxw_viewWillAppear:);
[SwizzManagerswizzMethodForClass:[selfclass]originSel:originSel1newSel:newSel1];
SELoriginSel2 =@selector(viewDidDisappear:);
SELnewSel2 =@selector(hxw_viewDidDisappear:);
[SwizzManagerswizzMethodForClass:[selfclass]originSel:originSel2newSel:newSel2];
});
}
- (void)hxw_viewDidLoad{
[self hxw_viewDidLoad];
[__analysisDelegateUIViewController hxw_viewDidLoad:self];
}
- (void)hxw_viewWillAppear:(BOOL)animated{
[self hxw_viewWillAppear:animated];
[__analysisDelegateUIViewController hxw_viewWillAppear:animated viewController:self];
}
- (void)hxw_viewDidDisappear:(BOOL)animated{
[self hxw_viewDidDisappear:animated];
[__analysisDelegateUIViewController hxw_viewWillDisappear:animated viewController:self];
}
UITableView的相应事件,就是点击cell,而点击cell则是代理方法- (void)hxw_tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath,因此首先我们先要拿到实现代理的类,这就需要hook住setDelegate:这个方法拿到代理delegate,然后hook住代理的- (void)hxw_tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath,与手势的思路实现有点类似。具体实现如下:
+(void)load{3、集成
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
SELoriginSel =@selector(setDelegate:);
SELnewSel =@selector(hxw_setDelegate:);
[SwizzManagerswizzMethodForClass:[selfclass]originSel:originSelnewSel:newSel];
});
}
- (void)hxw_setDelegate:(id)delegate{
[self hxw_setDelegate:delegate];
Class delegateClass = [delegate class];
SEL originSel =@selector(tableView:didSelectRowAtIndexPath:);
NSString* newSelName = [NSStringstringWithFormat:@"hxw_%ld_%@",self.tag,NSStringFromSelector(originSel)];
SEL newSel =NSSelectorFromString(newSelName);
SEL impSel =@selector(hxw_tableView:didSelectRowAtIndexPath:);
///动态添加方法并交换
[SwizzManager swizzMethodForClass:delegateClass newSelImpClass:[self class] impSel:impSel originSel:originSel newSel:newSel];
}
- (void)hxw_tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath{
///执行原始方法,交换后为hxw_tag_tableView:didSelectRowAtIndexPath:,指向原始方法- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath这个的IMP
///这里的self实际上为delegate
NSString* selName = [NSStringstringWithFormat:@"hxw_%ld_%@",tableView.tag,NSStringFromSelector(@selector(tableView:didSelectRowAtIndexPath:))];
SEL swizzSel =NSSelectorFromString(selName);
if([self respondsToSelector:swizzSel]) {
IMP imp = [self methodForSelector:swizzSel];
void(*func)(id,SEL,id,id) = (void*)imp;
func(self, swizzSel, tableView, indexPath);
}
[__analysisDelegateUITableView hxw_tableView:tableView didSelectRowAtIndexPath:indexPath delagete:(id)self];
}
导入后只需要,在AppDelegate的didFinishlaunch方法中,调用[AnalysisManager shareInstance]初始化,并将代理设置给他,最后实现AnalysisDelegate的接口方法即可。
具体见demo
参考:
iOS无埋点数据统计实践
iOS 无痕埋点方案探究
iOS开发·runtime原理与实践: 方法交换篇(Method Swizzling)(iOS“黑魔法”,埋点统计,禁止UI控件连续点击,防奔溃处理)
推荐阅读
- 2020-04-07vue中Axios的封装和API接口的管理
- iOS中的Block
- 记录iOS生成分享图片的一些问题,根据UIView生成固定尺寸的分享图片
- 2019-08-29|2019-08-29 iOS13适配那点事
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- iOS面试题--基础
- 接口|axios接口报错-参数类型错误解决
- iOS|iOS 笔记之_时间戳 + DES 加密
- iOS,打Framework静态库
- 常用git命令总结