iOS 无侵入埋点的实践记录及思考
前言
在初期,没有做好埋点工作,或者着急赶时间,未能合理的做好埋点的工作,随着用户的增多,就会有分析用户的行为需求,统计某个页面用户的留存时间,虽然市面上有很多统计的SDK,他们大部分都是需要一个页面一个页面去添加,这对于程序猿来说是很不友好的,工作量又大,又不好管理,突然有一天需要修改某个地方,又要挨个去查找添加的埋点方法,去重新更改一遍。怎么样才能做好统一管理这些埋点的工作,让他们都统一到一块,又方便管理,是我们需要思考的,而且这样也节省了大家的时间。
思考
大家都知道
objective-c
是运行时的机制,所谓运行时就是将数据类型的确定由编译期延迟到了运行时,objective-c
是通过runtime
来实现的,它是一个非常强大的C语言
库 ,这个代码很早以前就开源了,想要了解objective-c
,可以看看Apple的Github和
Apple opensource开源代码。我们平时所编写的objective-c
代码,会在运行时转换成runtime
的c
语言代码,objective-c
通过runtime
创建类跟对象,并进行消息的发送与转发。
在做无侵入埋点的同时,我们需要了解下我们做埋点统计时需要在什么地方进行埋点统计。
以下是我的埋点思路
image.png
实践
我们确定了需要在什么地方进行埋点,接下来就开始实践,
Show me your code
首先我们写个工具类用来统计页面
///后期用到交换方法比较多,统一一个函数进行方法交换
- (void)ljl_exchangeMethodWithClass:(Class)cls
originalSEL:(SEL)originalSEL
changeSEL:(SEL)changeSEL{
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
Method changeMethod = class_getInstanceMethod(cls, changeSEL);
method_exchangeImplementations(originalMethod, changeMethod);
}
记录打印日志统一管理
- (void)recordHookClass:(Class)cls identifier:(NSString *)identifier{
NSLog(@"当前类名:%@",NSStringFromClass(cls));
NSLog(@"标识符:%@",identifier);
}
UIViewController统计
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
///获取
SEL willAppear = @selector(viewWillAppear:);
SEL hook_willAppear = @selector(hook_viewWillAppear:);
[[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:willAppear changeSEL:hook_willAppear];
SEL disappear = @selector(viewDidDisappear:);
SEL hook_disappear = @selector(hook_viewDidDisappear:);
[[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:disappear changeSEL:hook_disappear];
});
}
方法实现
- (void)hook_viewWillAppear:(BOOL)animated{
[[LJL_HookObjcLog logManage] recordHookClass:self.class identifier:@"进入"];
[self hook_viewWillAppear:animated];
}
- (void)hook_viewDidDisappear:(BOOL)animated{
[[LJL_HookObjcLog logManage] recordHookClass:self.class identifier:@"离开"];
[self hook_viewDidDisappear:animated];
}
此方案只是针对用户的停留时间及用户的进入次数,日志打印可按需求来统计,不同的需求进行不同的方式。
UITableView
UITableView
与UICollectionView
统计用户点击cell的方法都是在代理中,我们需要进行替换设置delegate
的方法,在、setDelegate:
方法中插入统计的代码,这里有个小坑,有的页面是没有实现didSelectRowAtIndexPath
,为了使得方法不交换可以判断下是否实现了didSelectRowAtIndexPath
再进行统计操作。
Code
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSEL = @selector(setDelegate:);
SEL changeSEL = @selector(hook_setDelegate:);
[[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:originalSEL changeSEL:changeSEL];
});
}
函数实现
- (void)hook_setDelegate:(id<UITableViewDelegate>)delegate{
[self hook_setDelegate:delegate];
Method didSelectmethod = class_getInstanceMethod(delegate.class, @selector(tableView:didSelectRowAtIndexPath:));
IMP hookIMP = class_getMethodImplementation(self.class, @selector(hook_tableView:didSelectRowAtIndexPath:));
char const* type = method_getTypeEncoding(didSelectmethod);
class_addMethod(delegate.class, @selector(hook_tableView:didSelectRowAtIndexPath:), hookIMP, type);
Method hookMethod = class_getInstanceMethod(delegate.class, @selector(hook_tableView:didSelectRowAtIndexPath:));
method_exchangeImplementations(didSelectmethod, hookMethod);
}
- (void)hook_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[[LJL_HookObjcLog logManage] recordHookClass:self.class identifier:[NSString stringWithFormat:@"%ld,%ld",indexPath.row,indexPath.section]];
[self hook_tableView:tableView didSelectRowAtIndexPath:indexPath];
}
UIButton的点击事件
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
///获取
SEL originalSEL = @selector(sendAction:to:forEvent:);
SEL changeSEL = @selector(hook_sendAction:to:forEvent:);
[[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:originalSEL changeSEL:changeSEL];
});
}
///MAKR:
- (void)hook_sendAction:(SEL)action
to:(nullable id)target
forEvent:(nullable UIEvent *)event{
[self hook_sendAction:action to:target forEvent:event];
///点击事件结束记录
if ([[event.allTouches anyObject]phase] == UITouchPhaseEnded) {
[[LJL_HookObjcLog logManage] recordLogActionHookClass:[target class] action:action identifier:@"UIButton"];
}
}
UIGestureRecognizer手势的Hook方法
@implementation UIGestureRecognizer (Log_Category)
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
///获取
SEL originalSEL = @selector(initWithTarget:action:);
SEL changeSEL = @selector(hook_initWithTarget:action:);
[[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:self originalSEL:originalSEL changeSEL:changeSEL];
});
}
- (instancetype)hook_initWithTarget:(nullable id)target action:(nullable SEL)action{
UIGestureRecognizer *gestureRecognizer = [self hook_initWithTarget:target action:action];
SEL changeSEL = @selector(hook_gestureAction:);
IMP hookIMP = class_getMethodImplementation(self.class, changeSEL);
const char *type = method_getTypeEncoding(class_getInstanceMethod([target class], action));
class_addMethod([target class], changeSEL, hookIMP, type);
[[LJL_HookObjcLog logManage]ljl_exchangeMethodWithClass:[target class] originalSEL:action changeSEL:changeSEL];
return gestureRecognizer;
}
- (void)hook_gestureAction:(id)sender{
[self hook_gestureAction:sender];
[[LJL_HookObjcLog logManage] recordLogActionHookClass:[sender class] action:@selector(action) identifier:@"手势"];
}
@end
思考总结
本文简单讲述无侵入埋点的统计方案,思路大致上是通过
Runtime
的运行机制,在运行期可以向类中新增或替换选择子所对应的方法实现。使用另外一份实现原有的方法实现。
在无侵入的基础上,即降低了代码的耦合,又方便了后期维护管理,相对于可视化埋点,方便简单,所有方式都会有优点与缺点。
本文描述的优点就是无侵入,低耦合,好管理维护。
缺点:有些页面是复用机制,比如cell的复用,一个控制器可能多次进入,需要我们做好统一管理的标识符,一个button的点击需要递归获取当前的控制器等操作。有些模块可能会出现统计不准确等因素,还有可能团队人员多了,定义的方法有时候都是一致的,这样对于这种无侵入的方式最终的效果是不太准确的。相比较可视化埋点,数据统计的更加合理,准确,维护成本略高
每个项目所要统计的内容不一致,精确的程度也不一样,都是各自的观点,本文只是自己的理解与记忆,如果你又什么更好的方案可以留言分享,谢谢。