1、背景
稀里嘩啦一大段
2、主要功能劃分
從整個流程來說,我把他劃分為下面幾個主要的功能,事件攔截
、viewPath獲取、數(shù)據(jù)上報、圈選功能,并在文章中會對每個功能進(jìn)行比較詳細(xì)的解析和代碼粘貼。
3、事件攔截
3.0、runtime核心功能
這里用到runtime的添加方法,交換方法
+(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector{
//添加交換實例方法
Class class = cls;
//添加交換類方法
//Class class = objc_getMetaClass(object_getClassName(cls));;
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzingMethod = class_getInstanceMethod(class, swizzingSelector);
BOOL addMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzingMethod),
method_getTypeEncoding(swizzingMethod));
//如果添加成功交換,交換實現(xiàn)
if (addMethod) {
class_replaceMethod(class,
swizzingSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzingMethod);
}
}
注意:添加實例方法和添加類方法有少許的區(qū)別,在使用是需要更具具體的場景進(jìn)行處理。具體原理可點擊這里查看。
3.1、頁面攔截
創(chuàng)建UIViewController的Category,在此對生命周期的方法進(jìn)行交換。
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalDidLoadSelector = @selector(viewDidLoad);
SEL swizzingDidLoadSelector = @selector(user_viewDidLoad);
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];
});
}
-(void)user_viewDidLoad
{
[self user_viewDidLoad];
//TODO:數(shù)據(jù)上傳代碼
}
小插曲:原本想對控制器的dealloc方法也行統(tǒng)一處理,但是在完成后發(fā)現(xiàn)和某個第三方有問題,在雙擊輸入框是出現(xiàn)crash,所以先不對這個進(jìn)行攔截。
3.2、按鈕攔截
對于系統(tǒng)的按鈕可直接對創(chuàng)建UIControl的Category分類,并對sendAction:to:forEvent:方法進(jìn)行攔截。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzingSelector = @selector(wm_sendAction:to:forEvent:);
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
- (void)wm_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
[self wm_sendAction:action to:target forEvent:event];
//TODO:數(shù)據(jù)上傳(GIO過濾)
}
注意:由于原先項目中集成了GIO統(tǒng)計一直還在用著,點擊方法中會攔截到GIO的growingHookTouch_xxxx方法,導(dǎo)致數(shù)據(jù)的多次上傳,所以在這邊對GIO的方法進(jìn)行過濾掉。
if ([NSStringFromSelector(action) hasPrefix:@"growingHookTouch"])return;
3.3、手勢攔截
確實在項目中使用點擊手勢的地方遠(yuǎn)比直接使用按鈕的地方多,由于這次埋點只對點擊事件處理所以也只UITapGestureRecognizer創(chuàng)建Category。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(initWithTarget:action:) swizzingSel:@selector(vi_initWithTarget:action:)];
});
}
- (instancetype)vi_initWithTarget:(nullable id)target action:(nullable SEL)action
{
UITapGestureRecognizer *selfGestureRecognizer = [self vi_initWithTarget:target action:action];
if (!target && !action) {
return selfGestureRecognizer;
}
if ([target isKindOfClass:[UIScrollView class]]) {
return selfGestureRecognizer;
}
Class class = [target class];
SEL sel = action;
//創(chuàng)建一個新的方法 方法名為 sel_name
NSString * sel_name = [NSString stringWithFormat:@"wm_%s_%@", class_getName([target class]),NSStringFromSelector(action)];
SEL sel_ = NSSelectorFromString(sel_name);
//添加一個方法 參數(shù):相應(yīng)手勢的類,添加的方法名,實現(xiàn)方法的函數(shù) responseUser_gesture
BOOL isAddMethod = class_addMethod(class,
sel_,
method_getImplementation(class_getInstanceMethod([self class], @selector(responseUser_gesture:))),
nil);
self.methodName = NSStringFromSelector(action);
//方法添加成功,原先的方法實現(xiàn) action -> 新的方法實現(xiàn) responseUser_gesture。
if (isAddMethod) {
Method selMethod = class_getInstanceMethod(class, sel);
Method sel_Method = class_getInstanceMethod(class, sel_);
method_exchangeImplementations(selMethod, sel_Method);
}
return selfGestureRecognizer;
}
-(void)responseUser_gesture:(UITapGestureRecognizer *)gesture
{
NSString * identifier = [NSString stringWithFormat:@"wm_%s_%@", class_getName([self class]),gesture.methodName];
//調(diào)用原方法
SEL sel = NSSelectorFromString(identifier);
if ([self respondsToSelector:sel]) {
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL,id) = (void *)imp;
func(self, sel,gesture);
}
}
//TODO:數(shù)據(jù)上報
解析:這邊做了兩部處理,有區(qū)別于按鈕點擊事件,按鈕是直接在觸發(fā)點擊事件消息轉(zhuǎn)發(fā)方法攔截,直接能到觸發(fā)的點。而這邊手勢是在創(chuàng)建手勢是,對點擊事件要再度處理。
第一步:在初始化方法中拿到實現(xiàn)方法
action,并動態(tài)創(chuàng)建一個方法和原本的action進(jìn)行交換。
第二步:在交互的實現(xiàn)中實現(xiàn)原先的action,然后在做數(shù)據(jù)上報處理。
小插曲:最開始想著對手勢的攔截就直接對UITapGestureRecognizer進(jìn)行處理,在攔截里面過其他的過濾,但后來發(fā)現(xiàn)是在太多系統(tǒng)的手勢,導(dǎo)致一些手勢直接失效,最后改成這樣。
3.4、列表攔截
對UITableView和UICollectionView的處理是對delegate進(jìn)行處理,過程類似于手勢。
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalAppearSelector = @selector(setDelegate:);
SEL swizzingAppearSelector = @selector(wm_collection_setDelegate:);
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
});
}
-(void)wm_tableView_setDelegate:(id<UITableViewDelegate>)delegate
{
[self wm_tableView_setDelegate:delegate];
SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
SEL sel_ = NSSelectorFromString([NSString stringWithFormat:@"%@_%@_%ld", NSStringFromClass([delegate class]), NSStringFromClass([self class]),(long)self.tag]);
//因為 tableView:didSelectRowAtIndexPath:方法是optional的,所以沒有實現(xiàn)的時候直接return
if (![self isContainSel:sel inClass:[delegate class]]) {
return;
}
BOOL addsuccess = class_addMethod([delegate class],
sel_,
method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))),
nil);
//如果添加成功了就直接交換實現(xiàn), 如果沒有添加成功,說明之前已經(jīng)添加過并交換過實現(xiàn)了
if (addsuccess) {
Method selMethod = class_getInstanceMethod([delegate class], sel);
Method sel_Method = class_getInstanceMethod([delegate class], sel_);
method_exchangeImplementations(selMethod, sel_Method);
}
}
- (void)user_collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@_%@_%ld", NSStringFromClass([self class]), NSStringFromClass([collectionView class]), (long)collectionView.tag]);
if ([self respondsToSelector:sel]) {
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL,id,id) = (void *)imp;
func(self, sel,collectionView,indexPath);
}
//TODO:數(shù)據(jù)上報
}
//判斷頁面是否實現(xiàn)了某個sel
- (BOOL)isContainSel:(SEL)sel inClass:(Class)class {
unsigned int count;
Method *methodList = class_copyMethodList(class,&count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) {
return YES;
}
}
return NO;
}
解析:實現(xiàn)思路和手勢的一樣,不過多書寫。
3.5、Alert攔截
Alert的攔截是直接對UIAlertAction點擊的按鈕進(jìn)行攔截。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WMMethodSwizzingTool swizzingClassForClass:[self class] originalSel:@selector(actionWithTitle:style:handler:) swizzingSel:@selector(wm_actionWithTitle:style:handler:)];
});
}
+ (instancetype)wm_actionWithTitle:(nullable NSString *)title style:(UIAlertActionStyle)style handler:(void (^ __nullable)(UIAlertAction *action))handler{
void (^handlerBlock)(UIAlertAction *action) = ^(UIAlertAction *action){
if (handler) {
handler(action);
}
//TODO:數(shù)據(jù)是否上報
}
UIAlertAction *alterAction = [UIAlertAction wm_actionWithTitle:title style:style handler:handlerBlock];
}
注意:這邊有點不一樣
1.這邊交換的類方法(上面也寫過區(qū)別)。
2..這里的點擊是block回調(diào),所以創(chuàng)建了一個中間block進(jìn)行處理。
3..數(shù)據(jù)上報這塊,直接給到取消、確認(rèn)是完全沒有意義的,所以給UIAlertAction添加了個屬性,記錄這個彈框的更多信息,
已定位業(yè)務(wù)。
UIAlertController給UIAlertAction添加的屬性賦值。(UIAlertAction添加屬性方法略)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[WMMethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(addAction:) swizzingSel:@selector(wm_addAction:)];
});
}
- (void)wm_addAction:(UIAlertAction *)action {
[self wm_addAction:action];
action.alertControllerActionPath = [NSString stringWithFormat:@"%@/%@",self.title,self.message];
}
小總結(jié):
這期攔截代碼到這里基本上就沒了。其中完成了:
1.進(jìn)頁面有統(tǒng)一的地方得到當(dāng)前的控制器。
2.點擊(按鈕,手勢)有統(tǒng)一的響應(yīng)方法的地方。
3.列表點擊有統(tǒng)一響應(yīng)的地方。
4.彈框有統(tǒng)一的響應(yīng),并能拿到彈框信息。
4、ViewPath獲取
先放出ViewPath格式:
普通路徑:
WMMineViewController[0]/UIView[0]/UITableView[0]/UIView[1]/WMMineTopInfoView[0]/UIView[0]
復(fù)雜路徑:
WMHomePageViewController[0]/UIView[0]/UICollectionView[0]/WMHomePageBannerCell#[1,0]/UIView[0]/SDCycleScrollView[0]/UICollectionView[0]/SDCollectionViewCell#[0,1]
ViewPath是每個組件的唯一路徑,大數(shù)據(jù)通過ViewPath來確定當(dāng)前點擊的是什么(圈選來告訴這個ViewPath是什么),然后進(jìn)行數(shù)據(jù)分析。
直接上代碼:
4.1、第一步
UIView的Category,獲取某個view在同一級別的深度,上面路勁中的[0]。
- (NSString *)obtainSameSuperViewSameClassViewTreeIndexPath
{
NSString *classStr = NSStringFromClass([self class]);
//cell的子view
//UITableView 特殊的superview (UITableViewContentView)
//UICollectionViewCell
BOOL shouldUseSuperView =
([classStr isEqualToString:@"UITableViewCellContentView"]) ||
([[self.superview class] isKindOfClass:[UITableViewCell class]])||
([[self.superview class] isKindOfClass:[UICollectionViewCell class]]);
if (shouldUseSuperView) {
return [self obtainIndexPathByView:self.superview];
}else {
return [self obtainIndexPathByView:self];
}
}
- (NSString *)obtainIndexPathByView:(UIView *)view
{
// NSInteger viewTreeNodeDepth = NSIntegerMin;//所有類型 深度
NSInteger sameViewTreeNodeDepth = -1;//相同類型 深度(默認(rèn)-1)
NSString *classStr = NSStringFromClass([view class]);
NSMutableArray *sameClassArr = [[NSMutableArray alloc]init];
//所處父view的全部subviews根節(jié)點深度
for (NSInteger index =0; index < view.superview.subviews.count; index ++) {
//同類型
if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){
[sameClassArr addObject:view.superview.subviews[index]];
}
}
//所處父view的同類型subviews根節(jié)點深度
for (NSInteger index =0; index < sameClassArr.count; index ++) {
if (view == sameClassArr[index]) {
sameViewTreeNodeDepth = index;
break;
}
}
return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth];
}
這里是做同類控件的深度。
4.2、第二步
UIResponder的Category,通過響應(yīng)鏈獲取完整的路徑。
- (NSString *)generateViewPath
{
NSString *spointViewPath;
if ([self isKindOfClass:[UIView class]]) {
UIView *view1 = (id)self;
NSMutableString *str = [NSMutableString string];
str = [[NSStringFromClass(view1.class) stringByAppendingFormat:@"%@",str] mutableCopy];
//將viewPath放入 accessibilityIdentifier ,如果存在直接返回,優(yōu)化性能。
if (view1.accessibilityIdentifier) {
return view1.accessibilityIdentifier;
}else{
[str appendFormat:@"%@",[self getIndexPathForView:view1]];
}
UIView *view = (id)self;
while (view.nextResponder) {
if ([view.class isSubclassOfClass:[UIViewController class]]) {
break;
}
if ([view isMemberOfClass:[MMPopupWindow class]]) {
spointViewPath = [NSString stringWithFormat:@"%@",[(MMPopupView *)view class]];
break;
}
str = [[@"/" stringByAppendingFormat:@"%@",str] mutableCopy];
view = (id)view.nextResponder;
NSString *sameViewTreeNode1 = @"[0]";
if ([view isKindOfClass:[UIView class]]) {
sameViewTreeNode1 = [self getIndexPathForView:view];
}
str = [[sameViewTreeNode1 stringByAppendingString:str] mutableCopy];
str = [[NSStringFromClass(view.class) stringByAppendingFormat:@"%@",str] mutableCopy];
}
spointViewPath = [NSString stringWithFormat:@"%@",str];
view1.accessibilityIdentifier = spointViewPath;
}
return spointViewPath;
}
- (NSString *)getIndexPathForView:(UIView *)cellView {
NSString *cellIndexPath = [NSString string];
if ([cellView.superview isKindOfClass:[UICollectionView class]]&&[self isKindOfClass:[UICollectionViewCell class]]) {
UICollectionView *collectionView = (UICollectionView *)cellView.superview;
NSIndexPath *indexPath = [collectionView indexPathForCell:(UICollectionViewCell *)cellView];
cellIndexPath = [NSString stringWithFormat:@"#[%ld,%ld]",(long)indexPath.section,(long)indexPath.row];
}else if ([cellView.superview isKindOfClass:[UITableView class]]&&[self isKindOfClass:[UITableViewCell class]]) {
UITableView *tableView = (UITableView *)cellView.superview;
NSIndexPath *indexPath = [tableView indexPathForCell:(UITableViewCell *)cellView];
cellIndexPath = [NSString stringWithFormat:@"#[%ld,%ld]",(long)indexPath.section,(long)indexPath.row];
}else{
cellIndexPath = [NSString stringWithFormat:@"[%@]",[cellView obtainSameSuperViewSameClassViewTreeIndexPath]];
}
return cellIndexPath;
}
為了方面后面的圈選統(tǒng)一,在這邊直接在cell的后面添加了所在位置,就不必再各個上傳數(shù)據(jù)的地方在拼接上去。
5、圈選功能
圈選代碼太多詳細(xì)內(nèi)容查看demo
這里只提示一些注意點。
1.圈選得到的路徑和上傳得到的路徑必須一致。
2.圈選根據(jù)要求只對能響應(yīng)事件的控件進(jìn)行圈選。
3.圈選的內(nèi)容可能沒有事件但能響應(yīng)事件也能圈選。
4.一些第三方輪播庫的index并不確定,需要組件里面頁碼實現(xiàn)的規(guī)則進(jìn)行特殊計算。
5.出現(xiàn)圈選icon可通過掃scheme二維碼實現(xiàn)或項目中隱蔽的入口。
6、數(shù)據(jù)上傳
數(shù)據(jù)上傳這塊更具自己服務(wù)所需數(shù)據(jù)處理就好,總結(jié)一下幾點。
1.網(wǎng)絡(luò)這塊直接通過AFN再次封裝,不使用項目中現(xiàn)有的減少依賴。
2.上傳的數(shù)據(jù)模型和服務(wù)約定就好。
7、總結(jié)
以上能實現(xiàn)基本的實時埋點和實時上傳的功能,也是目前公司項目做得第一期所有功能。感謝網(wǎng)絡(luò)上許多文章,后續(xù)有更新再補充,希望對你有幫助,謝謝閱讀。
項目完整demo