iOS埋點之無痕埋點實踐

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ù)。

UIAlertControllerUIAlertAction添加的屬性賦值。(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • https://mp.weixin.qq.com/s/u-HmmrSAgtER1N2pKxCm0A 隨著公司業(yè)務(wù)的...
    海浪萌物閱讀 3,150評論 1 1
  • 前言 最近跟同事花了點時間來思考可視化埋點,并沒有什么突破性的進(jìn)展,不過市面上很多關(guān)于可視化埋點的技術(shù)文章都在講達(dá)...
    daixunry閱讀 8,246評論 1 38
  • 簡單介紹一下 AOP 無痕埋點最重要的技術(shù)是將埋點代碼從業(yè)務(wù)代碼中剝離,放到獨立的模塊中的技術(shù)。寫業(yè)務(wù)的同學(xué)只需按...
    Magic_Unique閱讀 8,187評論 16 53
  • 今年真是比較痛苦的一年,股市虧了一整年,對自己的水平認(rèn)不清,過于幻想,對市場也是過于幻想。只想著一把掙錢出來,卻沒...
    巴克萌萌噠閱讀 301評論 0 0
  • 都市里的人們和平常一樣為了生計奔波,在這里有著魔王,也有無數(shù)為了打敗魔王而努力的勇者。雖然人類在魔王軍的進(jìn)攻下一...
    0大神來也0閱讀 265評論 1 2

友情鏈接更多精彩內(nèi)容