本文章系作者原创文章,如需转载学习,请注明该文章的原始出处和网址链接。
在阅读的过程中,如若对该文章有不懂或值得优化的建议,欢迎大家加QQ:690091622 进行技术交流和探讨。
前言:
前几日做项目,需要做这样的一个功能:
记录应用Crash之前用户操作的最后20步
看到这样的需求,第一感觉就是有些懵,excuse me? 用户咋操作的我咋知道???应用啥时候Crash我咋知道???
最后,经过各方查找资料,终于搞定了。
先不多说,放一张控制台输出的运行结果的截图。
1. 技术原理
1.1 Method-Swizzling
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。
利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法hook的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
- 用
method_exchangeImplementations
方法来交换2个方法中的IMP, - 用
class_replaceMethod
方法来修改类, - 用
method_setImplementation
方法来直接设置某个方法的IMP,
其实,就是在程序运行中偷换了selector的IMP,如下图所示:
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);
}
- (nullable UIViewController *)hook_popViewControllerAnimated:(BOOL)animated
{
NSString \*popDetailInfo = [NSString stringWithFormat:@"%@ - %@", NSStringFromClass([self class]), @"pop"];
NSLog(@"%@", popDetailInfo);
return [self hook_popViewControllerAnimated:animated];
}
@end
至此,核心代码已经完成了。
那么如何使用该功能来记录用户操作轨迹呢?
在appDelegate.m文件中的application:didFinishLaunchingWithOptions:
添加如下四行代码: <pre>
[UIApplication hookUIApplication];
[UIViewController hookUIViewController];
[UINavigationController hookUINavigationController_push];
[UINavigationController hookUINavigationController_pop];
</pre>
启动程序,并观察控制台输出,神奇的事情将会发生,用户的每一次操作和页面跳转都会被记录下来。
提醒
1.UITabBarItem
当用户点击了UITabBarItem时,会同时记录三次事件,分别是:
_buttonDown:
_buttonUp:
_tabBarItemClicked:
所以,对于这三个事件,我们可以只需保留一个,将其他两个在记录的时候过滤掉。若记录空间有限,过滤掉冗余的信息,这样可以在有限的记录空间上记录更多的用户操作数据。
总结
1.hook方式非常强大,几乎可以截取任何用户想截取的消息事件,但是,每次触发hook,必然存在置换IMP整个过程,频繁的置换IMP必然会影响到应用及手机资源的消耗,不到非不得已,建议少用。
2.什么时候用hook的方式来埋点呢?例如,当应用有10个页面,而我们只需在其中两个页面上埋点,那么就没必要用这种方式了。具体什么时候用,由开发者根据项目实际需求来权衡,我们的原则就是要力图资源消耗最少。
3.对于View上的手势触摸事件touchBegan:withEvent:
等,这种方式截取不到消息。之所以暂时不做,也是因为消耗的问题,因为苹果手机都是触摸屏的,每进行一次触摸屏幕,不管会不会产生交互事件都会触发该事件的。有兴趣的小伙伴可以根据以上提供的思路来自己尝试实现下,测试下系统消耗,看适不适合来做。
- 以上内容都是手动输入的,文字个别错误还请见谅。
- 如果技术说明上有不正确之处,欢迎批评指正,可在下方留言,谢谢!