iOS用户行为追踪——无侵入埋点

  本文章系作者原创文章,如需转载学习,请注明该文章的原始出处和网址链接。
  在阅读的过程中,如若对该文章有不懂或值得优化的建议,欢迎大家加QQ:690091622 进行技术交流和探讨。


前言:
  前几日做项目,需要做这样的一个功能:
    记录应用Crash之前用户操作的最后20步
  看到这样的需求,第一感觉就是有些懵,excuse me? 用户咋操作的我咋知道???应用啥时候Crash我咋知道???

  最后,经过各方查找资料,终于搞定了。
  先不多说,放一张控制台输出的运行结果的截图。


User_Trace_Sequence.jpg

1. 技术原理

 1.1 Method-Swizzling

  在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。
  利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法hook的目的。
  每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。


IMP.jpg
  1. 用 method_exchangeImplementations 方法来交换2个方法中的IMP,
  2. 用 class_replaceMethod 方法来修改类,
  3. 用 method_setImplementation 方法来直接设置某个方法的IMP,

  其实,就是在程序运行中偷换了selector的IMP,如下图所示:


IMP_exchange.jpg
 1.2 Target-Action

  对于一个给定的事件,UIControl会调用sendAction:to:forEvent:来将行为消息转发到UIApplication对象,再由UIApplication对象调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上,而如果我们没有指定target,则会将事件分发到响应链上第一个想处理消息的对象上。
  而如果子类想监控或修改这种行为的话,则可以重写这个方法。

2.实现分析

  用户的操作行为轨迹在应用上的体现无非就是以下这几种情况:

  • 点击了哪个按钮
  • 哪个页面跳转到哪个页面
  • 当前停留在是哪个界面 

  1. 对于我们需要实现的功能中关于记录用户交互的操作,我们使用runtime中的方法hook下sendAction:to:forEvent:便可以知道用户进行了什么样的交互操作。
这个方法对UIControl及继承于UIControl而实现的子类对象是有效的,比如UIButton、UISlider、UIDatePicker、UISegmentControl等。
  2. iOS中页面切换有两种方式:UIViewController中的presentViewController:animated:dismissViewController:completion:;UINavigationController中的pushViewController:animated:popViewControllerAnimated:
  但是,对于UIViewController来说,我们不对这两个方法hook,因为页面跳来跳去,记录下来的各种数据会很多很乱,不利于后续查看。所以hook下ViewDidAppear:这个方法知道哪个页面显示了就足够了,而所有显示的页面按时间顺序连成序列,便是用户操作后应用中的页面跳转的轨迹。

  这个解决方案看起来很不错,这样既没有在项目中到处插入埋点函数,也没有给项目增加多少代码量,是一个两全其美的办法。

3. 代码实现

  以下是对三个类进行hook的主要实现代码。

 3.1. UIApplication

@interface UIApplication (HLCHook)
+ (void)hookUIApplication;
@end


@implementation UIApplication (HLCHook)
+ (void)hookUIApplication
{
Method controlMethod = class_getInstanceMethod([UIApplication class], @selector(sendAction:to:from:forEvent:));
Method hookMethod = class_getInstanceMethod([self class], @selector(hook_sendAction:to:from:forEvent:));
method_exchangeImplementations(controlMethod, hookMethod);
}


- (BOOL)hook_sendAction:(SEL)action to:(nullable id)target from:(nullable id)sender forEvent:(nullable UIEvent *)event;
{
NSString *actionDetailInfo = [NSString stringWithFormat:@" %@ - %@ - %@", NSStringFromClass([target class]), NSStringFromClass([sender class]), NSStringFromSelector(action)];
NSLog(@"%@", actionDetailInfo);
return [self hook_sendAction:action to:target from:sender forEvent:event];
}
@end

 3.2. UIViewController

@interface UIViewController (HLCHook)
+ (void)hookUIViewController;
@end


@implementation UIViewController (HLCHook)
+ (void)hookUIViewController
{
Method appearMethod = class_getInstanceMethod([self class], @selector(viewDidAppear:));
Method hookMethod = class_getInstanceMethod([self class], @selector(hook_ViewDidAppear:));
method_exchangeImplementations(appearMethod, hookMethod);
}


- (void)hook_ViewDidAppear:(BOOL)animated
{
NSString *appearDetailInfo = [NSString stringWithFormat:@" %@ - %@", NSStringFromClass([self class]), @"didAppear"];
NSLog(@"%@", appearDetailInfo);
[self hook_ViewDidAppear:animated];
}
@end

 3.3. UINavigatinoController

@interface UINavigationController (HLCHook)
+ (void)hookUINavigationController_push;
+ (void)hookUINavigationController_pop;
@end


@implementation UINavigationController (HLCHook)
+ (void)hookUINavigationController_push
{
Method pushMethod = class_getInstanceMethod([self class], @selector(pushViewController:animated:));
Method hookMethod = class_getInstanceMethod([self class], @selector(hook_pushViewController:animated:));
method_exchangeImplementations(pushMethod, hookMethod);
}


- (void)hook_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
NSString *popDetailInfo = [NSString stringWithFormat: @"%@ - %@ - %@", NSStringFromClass([self class]), @"push", NSStringFromClass([viewController class])];
NSLog(@"%@", popDetailInfo);
[self hook_pushViewController:viewController animated:animated];
}


+ (void)hookUINavigationController_pop
{
Method popMethod = class_getInstanceMethod([self class], @selector(popViewControllerAnimated:));
Method hookMethod = class_getInstanceMethod([self class], @selector(hook_popViewControllerAnimated:));
method_exchangeImplementations(popMethod, hookMethod);
}


- (void)hook_popViewControllerAnimated:(BOOL)animated
{
NSString *popDetailInfo = [NSString stringWithFormat:@"%@ - %@", NSStringFromClass([self class]), @"pop"];
NSLog(@"%@", popDetailInfo);
[self hook_popViewControllerAnimated:animated];
}
@end


  至此,核心代码已经完成了。
  那么如何使用该功能来记录用户操作轨迹呢?
  在appDelegate.m文件中的 application:didFinishLaunchingWithOptions: 添加如下四行代码: 
    [UIApplication hookUIApplication];
[UIViewController hookUIViewController];
[UINavigationController hookUINavigationController_push];
[UINavigationController hookUINavigationController_pop];

  启动程序,并观察控制台输出,神奇的事情将会发生,用户的每一次操作和页面跳转都会被记录下来。

提醒

1.UITabBarItem
  当用户点击了UITabBarItem时,会同时记录三次事件,分别是:

  • _buttonDown:
  • _buttonUp:
  • _tabBarItemClicked:

  所以,对于这三个事件,我们可以只需保留一个,将其他两个在记录的时候过滤掉。若记录空间有限,过滤掉冗余的信息,这样可以在有限的记录空间上记录更多的用户操作数据。

总结

  1.hook方式非常强大,几乎可以截取任何用户想截取的消息事件,但是,每次触发hook,必然存在置换IMP整个过程,频繁的置换IMP必然会影响到应用及手机资源的消耗,不到非不得已,建议少用。
  2.什么时候用hook的方式来埋点呢?例如,当应用有10个页面,而我们只需在其中两个页面上埋点,那么就没必要用这种方式了。具体什么时候用,由开发者根据项目实际需求来权衡,我们的原则就是要力图资源消耗最少。
  3.对于View上的手势触摸事件touchBegan:withEvent:等,这种方式截取不到消息。之所以暂时不做,也是因为消耗的问题,因为苹果手机都是触摸屏的,每进行一次触摸屏幕,不管会不会产生交互事件都会触发该事件的。有兴趣的小伙伴可以根据以上提供的思路来自己尝试实现下,测试下系统消耗,看适不适合来做。

    • 2
      点赞
    • 8
      收藏
      觉得还不错? 一键收藏
    • 0
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值