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:等,這種方式截取不到消息。之所以暫時不做,也是因為消耗的問題,因為蘋果手機都是觸摸屏的,每進行一次觸摸屏幕,不管會不會產生交互事件都會觸發該事件的。有興趣的小伙伴可以根據以上提供的思路來自己嘗試實現下,測試下系統消耗,看適不適合來做。

  • 以上內容都是手動輸入的,文字個別錯誤還請見諒。
  • 如果技術說明上有不正確之處,歡迎批評指正,可在下方留言,謝謝!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 0 引言 最近在負責公司的HubbleData的埋點SDK的開發任務,產品的雛形其實在幾年前就已經有了,公司內部的...
    魯冰閱讀 10,153評論 9 61
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,789評論 0 9
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,552評論 25 708
  • 本篇文章是講述 iOS 無埋點數據收集 SDK 系列的第二篇。在第一篇 中主要介紹了 SDK 整體實現思路以及...
    zerygao閱讀 12,282評論 4 64
  • 現在的時間已經是三月二十六號凌晨05分吧。心情特別的興奮,嘻嘻,躺在床上還偷笑。我剛剛和曉欣第一次去電影院看電影,...
    烏哩馬叉的xiao熊閱讀 183評論 0 1