前言
最近業(yè)務(wù)需要加入一大批埋點統(tǒng)計事件,這個頁面添加一點代碼那個頁面添加一點代碼,各個頁面內(nèi)耦合了大量的無關(guān)業(yè)務(wù)的埋點代碼使得頁面雜亂不堪,所以想尋找一個比較好的方法來解決這個事情。
探索
經(jīng)過一番考慮想到如下方案:
1、每個業(yè)務(wù)頁面添加一個埋點類,單獨將埋點的方法提取到這個類中。
2、利用runtime在底層進行方法攔截,從而添加埋點代碼。
最后采用了第2種方案。
技術(shù)原理
一、Method-Swizzling
oc中的方法調(diào)用其實是向一個對象發(fā)送消息 ,利用oc的動態(tài)性可以實現(xiàn)方法的交換。
1、用 method_exchangeImplementations 方法來交換2個方法中的IMP
2、用 class_replaceMethod 方法來替換類的方法,
3、用 method_setImplementation 方法來直接設(shè)置某個方法的IMP
二、Target-Action
按鈕的點擊事件,UIControl會調(diào)用sendAction:to:forEvent:來將行為消息轉(zhuǎn)發(fā)到UIApplication,再由UIApplication調(diào)用其sendAction:to:fromSender:forEvent:方法來將消息分發(fā)到指定的target上。
分析及實現(xiàn)
一、 需要添加埋點統(tǒng)計的地方:
1、button相關(guān)的點擊事件
2、頁面進入、頁面推出
3、tableView的點擊
4、collectionView的點擊
5、手勢相關(guān)事件
二、分析
1、對于用戶交互的操作,我們使用runtime 對應(yīng)的方法hook 下sendAction:to:forEvent:便可以得到進行的交互操作。
這個方法對UIControl及繼承UIControl的子類對象有效,如:UIButton、UISlider等。
2、對于UIViewController,hook下ViewDidAppear:這個方法知道哪個頁面顯示了就足夠了。
3、對于tableview及collectionview,我們hook下setDelegate:方法。檢測其有沒有實現(xiàn)對應(yīng)的點擊代理,因為tableView:didSelectRowAtIndexPath:及collectionView:didSelectItemAtIndexPath:是option的不是必須要實現(xiàn)的。
4、對于手勢,我們在創(chuàng)建的時候進行hook,方法為initWithTarget:action:。
三、代碼實現(xiàn)
1、UIControl+Track
@implementation UIControl (Track)
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzingSelector = @selector(dk_sendAction:to:forEvent:);
[DKMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
- (void)dk_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[self dk_sendAction:action to:target forEvent:event];
//埋點實現(xiàn)區(qū)域====
}
@end
2、UIViewController+Track
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalDidLoadSel = @selector(viewDidLoad);
SEL swizzingDidLoadSel = @selector(dk_viewDidLoad);
[DKMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSel swizzingSel:swizzingDidLoadSel];
});
}
- (void)dk_viewDidLoad {
[self dk_viewDidLoad];
//埋點實現(xiàn)區(qū)域====
}
3、UITableView+Track
@implementation UITableView (Track)
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(setDelegate:);
SEL swizzingSelector = @selector(dk_setDelegate:);
[DKMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
- (void)dk_setDelegate:(id<UITableViewDelegate>)delegate {
[self dk_setDelegate:delegate];
SEL originalSel = @selector(tableView:didSelectRowAtIndexPath:);
SEL swizzingSel = NSSelectorFromString([NSString stringWithFormat:@"%@/%@", NSStringFromClass([delegate class]),@(self.tag)]);
//didSelectRowAtIndexPath不一定要實現(xiàn),未實現(xiàn)在跳過
if (![DKMethodSwizzingTool isContainSel:originalSel class:[delegate class]]) {
return;
}
BOOL addMethod = class_addMethod([delegate class], swizzingSel, method_getImplementation(class_getInstanceMethod([self class], @selector(dk_tableView:didSelectRowAtIndexPath:))), nil);
if (addMethod) {
Method originalMetod = class_getInstanceMethod([delegate class], originalSel);
Method swizzingMethod = class_getInstanceMethod([delegate class], swizzingSel);
method_exchangeImplementations(originalMetod, swizzingMethod);
}
}
- (void)dk_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = [NSString stringWithFormat:@"%@/%@", NSStringFromClass([self class]),@(tableView.tag)];
SEL sel = NSSelectorFromString(identifier);
if ([self respondsToSelector:sel]) {
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL,id,id) = (void *)imp;
func(self, sel,tableView,indexPath);
}
//埋點實現(xiàn)區(qū)域====
}
4、UICollectionView+Track同時拓展
5、UIGestureRecognizer+Track
結(jié)果
2019-05-07 15:29:57.725041+0800 DKDataTrackKitDemo[18913:1357822] eventName:button----eventParam:{
content = dictionary;
text = hahha;
tips = test;
}
2019-05-07 15:29:57.735695+0800 DKDataTrackKitDemo[18913:1357822] eventName:ViewController----eventParam:{
}
2019-05-07 15:29:59.830922+0800 DKDataTrackKitDemo[18913:1357822] eventName:tableView----eventParam:{
text = tableview;
}
2019-05-07 15:30:01.178838+0800 DKDataTrackKitDemo[18913:1357822] eventName:collectionview----eventParam:{
text = collectionView;
}
規(guī)則
其中用到的plist生成規(guī)則:
1、Action:
對應(yīng)的是UIControl。
每一個Action統(tǒng)計事件的匹配規(guī)則:頁面名稱/方法名/tag
參數(shù):EventName事件名、EventParam事件對應(yīng)的參數(shù)
2、TableView
對應(yīng)的是UITableView。
每一個TableView統(tǒng)計事件的匹配規(guī)則:頁面名稱/tag
參數(shù):viewcontroller是否從viewcontroller中取參數(shù)、 EventName事件名、EventParam事件對應(yīng)的參數(shù)
3、UICollectionView
規(guī)則同上。
4、UIGestureRecognizer
對應(yīng)的是手勢UIGestureRecognizer。
每一個UIGestureRecognizer統(tǒng)計事件的匹配規(guī)則:頁面名稱/方法名
參數(shù):EventName事件名、EventParam事件對應(yīng)的參數(shù)
5、UIViewController
規(guī)則同上。
寫在最后
hook方式非常強大,幾乎可以攔截你想要的全部方法,但是每次觸發(fā)hook必然會置換IMP的整個過程,頻繁的置換會造成資源的消耗,不到萬不得已,建議少用。
參考感謝:
https://blog.csdn.net/SandyLoo/article/details/81202105