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閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容

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