前言
最近業(yè)務(wù)需要加入一大批埋點(diǎn)統(tǒng)計(jì)事件灌具,這個(gè)頁(yè)面添加一點(diǎn)代碼那個(gè)頁(yè)面添加一點(diǎn)代碼沮峡,各個(gè)頁(yè)面內(nèi)耦合了大量的無(wú)關(guān)業(yè)務(wù)的埋點(diǎn)代碼使得頁(yè)面雜亂不堪,所以想尋找一個(gè)比較好的方法來(lái)解決這個(gè)事情。
探索
經(jīng)過(guò)一番考慮想到如下方案:
1眉厨、每個(gè)業(yè)務(wù)頁(yè)面添加一個(gè)埋點(diǎn)類(lèi)喊式,單獨(dú)將埋點(diǎn)的方法提取到這個(gè)類(lèi)中孵户。
2、利用runtime在底層進(jìn)行方法攔截岔留,從而添加埋點(diǎn)代碼夏哭。
最后采用了第2種方案。
技術(shù)原理
一献联、Method-Swizzling
oc中的方法調(diào)用其實(shí)是向一個(gè)對(duì)象發(fā)送消息 竖配,利用oc的動(dòng)態(tài)性可以實(shí)現(xiàn)方法的交換何址。
1、用 method_exchangeImplementations 方法來(lái)交換2個(gè)方法中的IMP
2进胯、用 class_replaceMethod 方法來(lái)替換類(lèi)的方法用爪,
3、用 method_setImplementation 方法來(lái)直接設(shè)置某個(gè)方法的IMP
二胁镐、Target-Action
按鈕的點(diǎn)擊事件偎血,UIControl會(huì)調(diào)用sendAction:to:forEvent:來(lái)將行為消息轉(zhuǎn)發(fā)到UIApplication,再由UIApplication調(diào)用其sendAction:to:fromSender:forEvent:方法來(lái)將消息分發(fā)到指定的target上盯漂。
分析及實(shí)現(xiàn)
一颇玷、 需要添加埋點(diǎn)統(tǒng)計(jì)的地方:
1、button相關(guān)的點(diǎn)擊事件
2就缆、頁(yè)面進(jìn)入帖渠、頁(yè)面推出
3、tableView的點(diǎn)擊
4竭宰、collectionView的點(diǎn)擊
5阿弃、手勢(shì)相關(guān)事件
二、分析
1羞延、對(duì)于用戶交互的操作渣淳,我們使用runtime 對(duì)應(yīng)的方法hook 下sendAction:to:forEvent:便可以得到進(jìn)行的交互操作。
這個(gè)方法對(duì)UIControl及繼承UIControl的子類(lèi)對(duì)象有效伴箩,如:UIButton入愧、UISlider等。
2嗤谚、對(duì)于UIViewController棺蛛,hook下ViewDidAppear:這個(gè)方法知道哪個(gè)頁(yè)面顯示了就足夠了。
3巩步、對(duì)于tableview及collectionview旁赊,我們hook下setDelegate:方法。檢測(cè)其有沒(méi)有實(shí)現(xiàn)對(duì)應(yīng)的點(diǎn)擊代理椅野,因?yàn)閠ableView:didSelectRowAtIndexPath:及collectionView:didSelectItemAtIndexPath:是option的不是必須要實(shí)現(xiàn)的终畅。
4、對(duì)于手勢(shì)竟闪,我們?cè)趧?chuàng)建的時(shí)候進(jìn)行hook离福,方法為initWithTarget:action:。
三炼蛤、代碼實(shí)現(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];
//埋點(diǎn)實(shí)現(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];
//埋點(diǎn)實(shí)現(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不一定要實(shí)現(xiàn)絮识,未實(shí)現(xiàn)在跳過(guò)
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);
}
//埋點(diǎn)實(shí)現(xiàn)區(qū)域====
}
4绿聘、UICollectionView+Track同時(shí)拓展
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:
對(duì)應(yīng)的是UIControl斜友。
每一個(gè)Action統(tǒng)計(jì)事件的匹配規(guī)則:頁(yè)面名稱(chēng)/方法名/tag
參數(shù):EventName事件名鳞尔、EventParam事件對(duì)應(yīng)的參數(shù)
2懦底、TableView
對(duì)應(yīng)的是UITableView辛馆。
每一個(gè)TableView統(tǒng)計(jì)事件的匹配規(guī)則:頁(yè)面名稱(chēng)/tag
參數(shù):viewcontroller是否從viewcontroller中取參數(shù)、 EventName事件名国拇、EventParam事件對(duì)應(yīng)的參數(shù)
3、UICollectionView
規(guī)則同上惯殊。
4酱吝、UIGestureRecognizer
對(duì)應(yīng)的是手勢(shì)UIGestureRecognizer。
每一個(gè)UIGestureRecognizer統(tǒng)計(jì)事件的匹配規(guī)則:頁(yè)面名稱(chēng)/方法名
參數(shù):EventName事件名土思、EventParam事件對(duì)應(yīng)的參數(shù)
5务热、UIViewController
規(guī)則同上。
寫(xiě)在最后
hook方式非常強(qiáng)大己儒,幾乎可以攔截你想要的全部方法崎岂,但是每次觸發(fā)hook必然會(huì)置換IMP的整個(gè)過(guò)程,頻繁的置換會(huì)造成資源的消耗闪湾,不到萬(wàn)不得已冲甘,建議少用。
參考感謝:
https://blog.csdn.net/SandyLoo/article/details/81202105