本文章系作者原創文章,如需轉載學習,請注明該文章的原始出處和網址鏈接。
??在閱讀的過程中,如若對該文章有不懂或值得優化的建議,歡迎大家加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:
等,這種方式截取不到消息。之所以暫時不做,也是因為消耗的問題,因為蘋果手機都是觸摸屏的,每進行一次觸摸屏幕,不管會不會產生交互事件都會觸發該事件的。有興趣的小伙伴可以根據以上提供的思路來自己嘗試實現下,測試下系統消耗,看適不適合來做。
- 以上內容都是手動輸入的,文字個別錯誤還請見諒。
- 如果技術說明上有不正確之處,歡迎批評指正,可在下方留言,謝謝!