轻舟
navigation
🏞️

无侵入埋点

常见的埋点方式包括代码埋点,可视化埋点和无埋点(全埋点)。
  • 代码埋点就是手写代码埋点,优点是精确,方便记录需要的值,缺点是工作量大,难以维护。
  • 可视化埋点,将埋点增加和修改工作可视化,提升埋点体验。
  • 无埋点,实际上是指的全埋点,埋点代码不会出现在业务代码中。缺点是成本高,解析复杂,优点是节省开发成本和维护成本。
这篇文章主要是分析无埋点如何实现。

运行时方法替换进行埋点

常用的埋点统计的是页面进度次数,页面停留时间,点击事件,通过运行时进行方法替换来插入埋点代码。
#import "SMHook.h" #import <objc/runtime.h> @implementation SMHook + (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector { Class class = classObject; // 得到被替换类的实例方法 Method fromMethod = class_getInstanceMethod(class, fromSelector); // 得到替换类的实例方法 Method toMethod = class_getInstanceMethod(class, toSelector); // class_addMethod 返回成功表示被替换的方法没实现,然后会通过 class_addMethod 方法先实现;返回失败则表示被替换方法已存在,可以直接进行 IMP 指针交换 if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) { // 进行方法的替换 class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod)); } else { // 交换 IMP 指针 method_exchangeImplementations(fromMethod, toMethod); } } @end
页面进入次数,页面停留时间都需要对ViewController的生命周期进行埋点,我们可以创建一个分类,在分类中处理相关方法替换的逻辑
@implementation UIViewController (logger) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 通过 @selector 获得被替换和替换方法的 SEL,作为 SMHook:hookClass:fromeSelector:toSelector 的参数传入 SEL fromSelectorAppear = @selector(viewWillAppear:); SEL toSelectorAppear = @selector(hook_viewWillAppear:); [SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear]; SEL fromSelectorDisappear = @selector(viewWillDisappear:); SEL toSelectorDisappear = @selector(hook_viewWillDisappear:); [SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear]; }); } - (void)hook_viewWillAppear:(BOOL)animated { // 先执行插入代码,再执行原 viewWillAppear 方法 [self insertToViewWillAppear]; [self hook_viewWillAppear:animated]; } - (void)hook_viewWillDisappear:(BOOL)animated { // 执行插入代码,再执行原 viewWillDisappear 方法 [self insertToViewWillDisappear]; [self hook_viewWillDisappear:animated]; } - (void)insertToViewWillAppear { // 在 ViewWillAppear 时进行日志的埋点 [[[[SMLogger create] message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]] classify:ProjectClassifyOperation] save]; } - (void)insertToViewWillDisappear { // 在 ViewWillDisappear 时进行日志的埋点 [[[[SMLogger create] message:[NSString stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]] classify:ProjectClassifyOperation] save]; } @end
对于点击事件来说,同样可以通过方法替换的方式无侵入埋点,最主要的是找到点击事件的方法sendAction:to:forEvent:。实现如下:
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 通过 @selector 获得被替换和替换方法的 SEL,作为 SMHook:hookClass:fromeSelector:toSelector 的参数传入 SEL fromSelector = @selector(sendAction:to:forEvent:); SEL toSelector = @selector(hook_sendAction:to:forEvent:); [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector]; }); } - (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { [self insertToSendAction:action to:target forEvent:event]; [self hook_sendAction:action to:target forEvent:event]; } - (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { // 日志记录 if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) { NSString *actionString = NSStringFromSelector(action); NSString *targetName = NSStringFromClass([target class]); [[[SMLogger create] message:[NSString stringWithFormat:@"%@ %@",targetName,actionString]] save]; } }
除了UIViewController和UIButton,UITableview可以使用hook setDelegate方法实现无侵入埋点,Gesture可以使用hook initWithTarget:action:方法来埋点。
无侵入埋点也是业界一大难题,目前还只是初级阶段,还有很长的路要走。运行时替换方法的方式也只是一种尝试,但是现实中业务代码太过复杂。同时,为了使无侵入的埋点能够覆盖得更全、准确度更高,代价往往是对埋点所需的标识维护成本不断增大。
badge