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);
} 
- (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:等,这种方式截取不到消息。之所以暂时不做,也是因为消耗的问题,因为苹果手机都是触摸屏的,每进行一次触摸屏幕,不管会不会产生交互事件都会触发该事件的。有兴趣的小伙伴可以根据以上提供的思路来自己尝试实现下,测试下系统消耗,看适不适合来做。

  • 以上内容都是手动输入的,文字个别错误还请见谅。
  • 如果技术说明上有不正确之处,欢迎批评指正,可在下方留言,谢谢!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 0 引言 最近在负责公司的HubbleData的埋点SDK的开发任务,产品的雏形其实在几年前就已经有了,公司内部的...
    鲁冰阅读 9,983评论 9 61
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • 本篇文章是讲述 iOS 无埋点数据收集 SDK 系列的第二篇。在第一篇 中主要介绍了 SDK 整体实现思路以及...
    zerygao阅读 12,178评论 4 64
  • 现在的时间已经是三月二十六号凌晨05分吧。心情特别的兴奋,嘻嘻,躺在床上还偷笑。我刚刚和晓欣第一次去电影院看电影,...
    乌哩马叉的xiao熊阅读 178评论 0 1