本文章系作者原創文章,如需轉載學習,請註明該文章的原始出處和網址鏈接。
在閱讀的過程中,如若對該文章有不懂或值得優化的建議,歡迎大家加QQ:690091622 進行技術交流和探討。
前言:
前幾日做項目,需要做這樣的一個功能:
記錄應用Crash之前用戶操作的最後20步
看到這樣的需求,第一感覺就是有些懵,excuse me? 用戶咋操作的我咋知道???應用啥時候Crash我咋知道???
最後,經過各方查找資料,終於搞定了。
先不多說,放一張控制檯輸出的運行結果的截圖。
在Objective-C中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector的名字。
利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法hook的目的。
每個類都有一個方法列表,存放着selector的名字和方法實現的映射關係。IMP有點類似函數指針,指向具體的Method實現。
method_exchangeImplementations
方法來交換2個方法中的IMP,class_replaceMethod
方法來修改類,method_setImplementation
方法來直接設置某個方法的IMP,其實,就是在程序運行中偷換了selector的IMP,如下圖所示:
對於一個給定的事件,UIControl會調用sendAction:to:forEvent:
來將行爲消息轉發到UIApplication對象,再由UIApplication對象調用其sendAction:to:fromSender:forEvent:
方法來將消息分發到指定的target上,而如果我們沒有指定target,則會將事件分發到響應鏈上第一個想處理消息的對象上。
而如果子類想監控或修改這種行爲的話,則可以重寫這個方法。
用戶的操作行爲軌跡在應用上的體現無非就是以下這幾種情況:
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:
這個方法知道哪個頁面顯示了就足夠了,而所有顯示的頁面按時間順序連成序列,便是用戶操作後應用中的頁面跳轉的軌跡。
這個解決方案看起來很不錯,這樣既沒有在項目中到處插入埋點函數,也沒有給項目增加多少代碼量,是一個兩全其美的辦法。
以下是對三個類進行hook的主要實現代碼。
@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
@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
@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);
}
- (void)hook_popViewControllerAnimated:(BOOL)animated
{
NSString *popDetailInfo = [NSString stringWithFormat:@"%@ - %@", NSStringFromClass([self class]), @"pop"];
NSLog(@"%@", popDetailInfo);
[self hook_popViewControllerAnimated:animated];
}
@end
application:didFinishLaunchingWithOptions:
添加如下四行代碼:
[UIApplication hookUIApplication];
[UIViewController hookUIViewController];
[UINavigationController hookUINavigationController_push];
[UINavigationController hookUINavigationController_pop];
1.UITabBarItem
當用戶點擊了UITabBarItem時,會同時記錄三次事件,分別是:
_buttonDown:
_buttonUp:
_tabBarItemClicked:
所以,對於這三個事件,我們可以只需保留一個,將其他兩個在記錄的時候過濾掉。若記錄空間有限,過濾掉冗餘的信息,這樣可以在有限的記錄空間上記錄更多的用戶操作數據。
1.hook方式非常強大,幾乎可以截取任何用戶想截取的消息事件,但是,每次觸發hook,必然存在置換IMP整個過程,頻繁的置換IMP必然會影響到應用及手機資源的消耗,不到非不得已,建議少用。
2.什麼時候用hook的方式來埋點呢?例如,當應用有10個頁面,而我們只需在其中兩個頁面上埋點,那麼就沒必要用這種方式了。具體什麼時候用,由開發者根據項目實際需求來權衡,我們的原則就是要力圖資源消耗最少。
3.對於View上的手勢觸摸事件touchBegan:withEvent:
等,這種方式截取不到消息。之所以暫時不做,也是因爲消耗的問題,因爲蘋果手機都是觸摸屏的,每進行一次觸摸屏幕,不管會不會產生交互事件都會觸發該事件的。有興趣的小夥伴可以根據以上提供的思路來自己嘗試實現下,測試下系統消耗,看適不適合來做。