iOS可視化埋點(diǎn)方案

前言

隨著公司業(yè)務(wù)的發(fā)展栏饮,數(shù)據(jù)的重要性日益體現(xiàn)出來。 數(shù)據(jù)埋點(diǎn)的準(zhǔn)確和全面性顯得尤為重要。
通過精準(zhǔn)和詳細(xì)的數(shù)據(jù)优炬,后面的分析才有意義颁井。隨著業(yè)務(wù)的不斷變化,動(dòng)態(tài)化埋點(diǎn)也越來越重要蠢护。

三大埋點(diǎn)方式

為了解決這些問題雅宾, 很多公司都提出自己的解決方案, 各中解決方案中葵硕,大體分為以下三種:

1眉抬、代碼埋點(diǎn)

由開發(fā)人員在觸發(fā)事件的具體方法里,植入多行代碼把需要上傳的參數(shù)上報(bào)至服務(wù)端懈凹。

2蜀变、可視化埋點(diǎn)

根據(jù)標(biāo)識(shí)來識(shí)別每一個(gè)事件, 針對(duì)指定的事件進(jìn)行取參埋點(diǎn)介评。而事件的標(biāo)識(shí)與參數(shù)信息都寫在配置表中库北,通過動(dòng)態(tài)下發(fā)配置表來實(shí)現(xiàn)埋點(diǎn)統(tǒng)計(jì)。

3们陆、無埋點(diǎn)

無埋點(diǎn)贤惯,也稱為“無痕埋點(diǎn)”,無埋點(diǎn)還有另一種叫法:全埋點(diǎn)棒掠。
前端的任意一個(gè)事件都被綁定一個(gè)標(biāo)識(shí),所有的事件都別記錄下來屁商。通過定期上傳記錄文件烟很,配合文件解析,解析出來我們想要的數(shù)據(jù)蜡镶, 并生成可視化報(bào)告供專業(yè)人員分析 雾袱,因此稱為“無埋點(diǎn)”統(tǒng)計(jì)。

無埋點(diǎn)方案目前已經(jīng)有神策埋點(diǎn)實(shí)現(xiàn)官还,另外考慮到“無埋點(diǎn)”的方案成本較高芹橡,并且后期解析也比較復(fù)雜,加上view_path的不確定性望伦,所以本文重點(diǎn)介紹“可視化埋點(diǎn)”的簡(jiǎn)單實(shí)現(xiàn)方式林说。

可視化埋點(diǎn)

可視化埋點(diǎn)并非完全拋棄了代碼埋點(diǎn),而是在代碼埋點(diǎn)的上層封裝的一套邏輯來代替手工埋點(diǎn)屯伞。理論上可視化的埋點(diǎn)也應(yīng)該封裝在埋點(diǎn)的SDK層腿箩,但是由于歷史原因,智聯(lián)的埋點(diǎn)SDK只封裝了緩存數(shù)據(jù)和上報(bào)數(shù)據(jù)這一層劣摇,所以我們可以在客戶端層面珠移,增加一層可視化埋點(diǎn)SDK。

大體架構(gòu)如下:


WX20181212-111410@2x.png

從業(yè)務(wù)架構(gòu)上來看,可視化埋點(diǎn)主要對(duì)頁(yè)面Out钧惧、In(PV)暇韧、按鈕等事件點(diǎn)擊(Action)、列表的滑動(dòng)浓瞪、點(diǎn)擊等(List)懈玻、手勢(shì)動(dòng)作(Gesture)進(jìn)行埋點(diǎn),就能覆蓋90%以上統(tǒng)計(jì)事件追逮。

要解決的問題

代碼埋點(diǎn)可以解決所有的自定義埋點(diǎn)酪刀,深入程度也是最高的,但是他有天然劣勢(shì)钮孵,就是每出現(xiàn)一個(gè)新的頁(yè)面骂倘,新的需求,都需要開發(fā)人員植入多行代碼把需要上傳的參數(shù)上報(bào)至服務(wù)端巴席,開發(fā)成本高历涝,效率低下,經(jīng)常出現(xiàn)業(yè)務(wù)開發(fā)需求一星期漾唉,埋點(diǎn)埋兩星期的情況荧库。

埋點(diǎn)需要解決的問題有:

1、重復(fù)埋點(diǎn)問題
如何才能動(dòng)態(tài)埋點(diǎn)赵刑,不需要每次需求都要特意去埋一次點(diǎn)荷辕,特別是那些頁(yè)面的進(jìn)出、停留時(shí)長(zhǎng)等的埋點(diǎn)蔽豺,重復(fù)的埋點(diǎn)徒增開發(fā)時(shí)間链方。

2、pageid(pagecode)不同而且無規(guī)律問題
雖然可視化埋點(diǎn)可以利用Hook原理铐懊,來解決這個(gè)問題邀桑,但由于統(tǒng)計(jì)的要求,每個(gè)頁(yè)面都自帶不同的pageid或者pagecode科乎,這樣一來無法利用父類的方式去一次性埋點(diǎn)壁畸,因?yàn)榧词雇ㄟ^繼承的方式,也無法做到每個(gè)子類都有不同的pageid茅茂。即使利用Hook原理捏萍,去Hook每一個(gè)頁(yè)面的Appear和DisAppear方法,也無法對(duì)這些不同的頁(yè)面注入不同的Pagecode玉吁,這樣唯一標(biāo)識(shí)又構(gòu)成了瓶頸照弥。

3、動(dòng)態(tài)埋點(diǎn)問題
即使進(jìn)行了代碼埋點(diǎn)进副,每個(gè)版本都進(jìn)行埋點(diǎn)这揣,但是卻無法對(duì)已經(jīng)上線的版本進(jìn)行埋點(diǎn)悔常,假如上線后有些埋點(diǎn)忘記埋了,就只能等到下個(gè)版本才能進(jìn)行埋點(diǎn)的添加给赞,是否有辦法做到動(dòng)態(tài)的下發(fā)配置來對(duì)線上版本增加埋點(diǎn)机打,而不需要發(fā)版呢?

4片迅、頁(yè)面OI先后順序問題
代碼埋點(diǎn)的方式残邀,如果想知道C頁(yè)面是從A頁(yè)面進(jìn)來的,還是從B頁(yè)面還是先A再B最后在進(jìn)入C的柑蛇,就得對(duì)每個(gè)頁(yè)面進(jìn)行一個(gè)傳值芥挣,而且這樣做還有一個(gè)弊端就是可能不知道用戶這個(gè)C頁(yè)面可能是這樣的:A->B->D->B->A->B->C,普通的代碼埋點(diǎn)只能知道是B到C耻台,卻不了解進(jìn)入C之前其實(shí)有很多前進(jìn)后退頁(yè)面的操作空免,這樣對(duì)數(shù)據(jù)分析可能就會(huì)有偏差,那是否有辦法做到自動(dòng)記錄頁(yè)面進(jìn)出的方案呢盆耽?

解決方案

唯一標(biāo)識(shí)的組成方式主要是又 target + action 來確定蹋砚, 即任何一個(gè)事件都存在一個(gè)target與action。 在此引入AOP編程摄杂,AOP(Aspect-Oriented-Programming)即面向切面編程的思想坝咐,基于 Runtime 的 Method Swizzling能力,來 hook 相應(yīng)的方法析恢,從而在hook方法中進(jìn)行統(tǒng)一的埋點(diǎn)處理墨坚。例如所有的按鈕被點(diǎn)擊時(shí),都會(huì)觸發(fā)UIApplication的sendAction方法映挂,我們hook這個(gè)方法框杜,即可攔截所有按鈕的點(diǎn)擊事件。

WX20181212-143303@2x.png

但是剛才問題2提到袖肥,只是單純的Hook,無法解決PageCode振劳、ActionCode如果埋入的問題椎组,即:“事件唯一標(biāo)識(shí)符“如何埋入的問題。所以在這里历恐,我們要利用一份配置表來管理這個(gè)“事件唯一標(biāo)識(shí)符“寸癌。
這里主要分為兩個(gè)部分 :

  • 事件的鎖定
    事件的鎖定主要是靠 “事件唯一標(biāo)識(shí)符”來鎖定,而事件的唯一標(biāo)識(shí)是由我們寫入配置表中的弱贼。這里分為兩種蒸苇,本地配置表和線上下載的配置表。

  • 埋點(diǎn)數(shù)據(jù)的上報(bào)吮旅。
    埋點(diǎn)數(shù)據(jù)的數(shù)據(jù)又分為兩種類型: 固定數(shù)據(jù)與可變的業(yè)務(wù)數(shù)據(jù)溪烤, 而固定數(shù)據(jù)我們可以直接寫到配置表中味咳,通過唯一標(biāo)識(shí)來獲取。而對(duì)于業(yè)務(wù)數(shù)據(jù)檬嘀,我是這么理解的:數(shù)據(jù)是有持有者的槽驶,例如我們Controller的一個(gè)屬性值,又或者數(shù)據(jù)再M(fèi)odel的某一個(gè)層級(jí)鸳兽。這么的話我們就可以通過KVC的的方式來遞歸獲取該屬性的值來取到業(yè)務(wù)數(shù)據(jù)掂铐。

整體解決方案

由于業(yè)務(wù)中的事件場(chǎng)景是多樣的,以iOS為例揍异,在此我以UIControl(Button全陨、Switch、TextField等都屬于Control), UITablview(CollectionView與TableView基本相同衷掷,Android里對(duì)應(yīng)的則是ListView)辱姨,UITapGesture,UIViewController的PV統(tǒng)計(jì)為例棍鳖,介紹一下具體思路炮叶。

  1. UIViewController PV統(tǒng)計(jì)
    頁(yè)面的統(tǒng)計(jì)較為簡(jiǎn)單,利用Method Swizzing hook 系統(tǒng)的viewDidLoad渡处, 直接通過頁(yè)面名稱即可鎖定頁(yè)面的展示代碼如下:
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalAppearSelector = @selector(viewWillAppear:);
        SEL swizzingAppearSelector = @selector(analysis_viewWillAppear:);
        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
        
        SEL originalDisappearSelector = @selector(viewWillDisappear:);
        SEL swizzingDisappearSelector = @selector(analysis_viewWillDisappear:);
        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDisappearSelector swizzingSel:swizzingDisappearSelector];
        
        SEL originalDidLoadSelector = @selector(viewDidLoad);
        SEL swizzingDidLoadSelector = @selector(analysis_viewDidLoad);
        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];
    });
}

Hook系統(tǒng)ViewDidLoad的方法大致如下:

- (void)analysis_viewDidLoad
{
    [self analysis_viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    //從配置表中取參數(shù)的過程 1 固定參數(shù)  2 業(yè)務(wù)參數(shù)(此處參數(shù)被target持有)
    NSString *identifier = [NSString stringWithFormat:@"%@", [self class]];
//    NSLog(@"identifier:%@",identifier);

    NSDictionary *dic = [[[ZPMDataContainer sharedInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier];
    if (dic) {
        NSString *pageid = dic[@"userDefined"][@"pageid"];
        NSString *pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary *pagePara = dic[@"pagePara"];
        
        __block NSMutableDictionary *uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            
            id value = [ZPMCapturePropertyTool captureVarforInstance:self withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
        
        NSLog(@"\n 事件唯一標(biāo)識(shí)為:%@ \n  pageid === %@,\n  pagename === %@,\n  pagepara === %@ \n", [self class], pageid, pagename, uploadDic);
    }
}
  1. UIControl 點(diǎn)擊統(tǒng)計(jì)
    主要通過hook sendAction:to:forEvent: 來實(shí)現(xiàn), 其唯一標(biāo)識(shí)符我們用 targetname/selector/tag來標(biāo)記镜悉,具體代碼如下:
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzingSelector = @selector(analysis_sendAction:to:forEvent:);
        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
    });
}

- (void)analysis_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    [self analysis_sendAction:action to:target forEvent:event];
    
    NSString *identifier = [NSString stringWithFormat:@"%@/%@", [target class], NSStringFromSelector(action)];
    NSDictionary *dic = [[[ZPMDataContainer sharedInstance].data objectForKey:@"ACTION"] objectForKey:identifier];
    if (dic) {
        
        NSString *eventid = dic[@"userDefined"][@"eventid"];
        NSString *targetname = dic[@"userDefined"][@"target"];
        NSString *pageid = dic[@"userDefined"][@"pageid"];
        NSString *pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary *pagePara = dic[@"pagePara"];
        __block NSMutableDictionary *uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL *_Nonnull stop) {
            
            id value = [ZPMCapturePropertyTool captureVarforInstance:target withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
        
        NSLog(@"\n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic);
    }
}
  1. TableView (CollectionView) 的點(diǎn)擊統(tǒng)計(jì)
    tablview的唯一標(biāo)識(shí), 我們使用 delegate.class/tableview.class/tableview.tag的組合來唯一鎖定医瘫。主要是通過hook setDelegate 方法侣肄, 在設(shè)置代理的時(shí)候再去交互 didSelect(點(diǎn)擊)、scrollViewDidScroll(滑動(dòng)列表)方法來實(shí)現(xiàn)醇份。具體代碼先不上了稼锅。

  2. gesture方式添加的的點(diǎn)擊統(tǒng)計(jì)
    gesture的事件,是通過 hook initWithTarget:action:方法來實(shí)現(xiàn)的僚纷, 事件的唯一標(biāo)識(shí)依然是target.class/actionname來鎖定的矩距。

從上面的代碼可以看出,這個(gè)pageid和eventid都不是寫死的怖竭,而且從一個(gè)字典里面獲取值锥债,如dic[@"userDefined"][@"pageid"],那么這個(gè)value從哪獲取呢痊臭,這里就要用到配置表了哮肚。

配置表結(jié)構(gòu)

配置表是一個(gè)json數(shù)據(jù)。 針對(duì)不同的場(chǎng)景 (UIControl , 頁(yè)面PV广匙, Tabeview, Gesture)都做了區(qū)分允趟, 用不同的key區(qū)別。 對(duì)于 "固定參數(shù)" 鸦致, 我們之間寫到配置表中潮剪,而對(duì)于業(yè)務(wù)參數(shù)涣楷, 我們之間寫清楚參數(shù)在業(yè)務(wù)內(nèi)的名字, 以及上傳時(shí)的 keyName鲁纠, 參數(shù)的持有者总棵。 通過Runtime + KVC來取值。 配置表可以是這個(gè)樣子:(僅供參考)

{
    "ACTION": {
        "ThirdViewController/jumpSecond:": {
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "234",
                "pagename": "button點(diǎn)擊改含,跳轉(zhuǎn)至下一個(gè)頁(yè)面"
            },
            "pagePara": {
                "testKey9": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        },
        
        "SecondViewController/back": {
            "userDefined": {
                "eventid": "201803074|965",
                "target": "second",
                "pageid": "235",
                "pagename": "button點(diǎn)擊情龄,返回"
            },
            "pagePara": {
                "testKey9": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    },
    
    "PAGEPV": {
        "HomeViewController": {
            "userDefined": {
                "pageid": "3156",
                "pagename": "首頁(yè)展示了"
            },
            "pagePara": {
                "testKey10": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        },
        "SecondViewController": {
            "userDefined": {
                "pageid": "4687",
                "pagename": "SecondView頁(yè)面展示"
            },
            "pagePara": {
                "testKey0": {
                    "propertyName": "age",
                    "propertyPath":"",
                    "containIn": "0"
                },
                "testKey1": {
                    "propertyName": "content",
                    "propertyPath":"",
                    "containIn": "0"
                },
                "testKey2": {
                    "propertyName": "propertyDic",
                    "propertyPath":"",
                    "containIn": "0"
                },
                "testKey3": {
                    "propertyName": "items",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    },
    "TABLEVIEW": {
        "ViewController/UITableView/0":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "238",
                "pagename": "tableview 被點(diǎn)擊"
            },
            "pagePara": {
                "user_grade": {
                    "propertyName": "grade",
                    "propertyPath":"",
                    "containIn": "1"
                }
            }
        }
    },
    
    "GESTURE": {
        "ViewController/gesture1clicked:":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "手勢(shì)1對(duì)應(yīng)的id",
                "pagename": "手勢(shì)1對(duì)應(yīng)的page name"
            },
            "pagePara": {
                "testKey1": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
                
            }
        },
        "ViewController/gesture2clicked:":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "手勢(shì)2對(duì)應(yīng)的id",
                "pagename": "手勢(shì)2對(duì)應(yīng)的page name"
            },
            "pagePara": {
                "testKey2": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
                
            }
        },
        
        "SecondViewController/gesture3clicked:":{
            "userDefined": {
                "eventid": "201803074|98",
                "target": "",
                "pageid": "gesture3clicked",
                "pagename": "手勢(shì)3對(duì)應(yīng)的page name"
            },
            "pagePara": {
                "user_age": {
                  
                }
                
            }
        }
    }
}

json最外層有四個(gè)Key, 分別為 ACTION PAGEPV TABLEVIEW GESTURE, 分別對(duì)應(yīng) UIControl的點(diǎn)擊,頁(yè)面PV,tableview cell點(diǎn)擊捍壤, Gesture 單擊事件的參數(shù)骤视。 每個(gè)key對(duì)應(yīng)的value為json格式,Json中的keys鹃觉, 即為唯一標(biāo)識(shí)符专酗。

標(biāo)識(shí)符下的json有兩個(gè)key :
userDefine指的固定數(shù)據(jù),即直接取值進(jìn)行上報(bào)盗扇。
而pagePara為業(yè)務(wù)參數(shù)祷肯。pagePara對(duì)應(yīng)的value也是一個(gè)json, json的keys疗隶,即上報(bào)的keys佑笋,value內(nèi)的json包含三個(gè)參數(shù):
propertyName為屬性名字,
containIn 參數(shù)只有0 斑鼻,1 兩種情況蒋纬,用來區(qū)分類似Tableview的Cell里面按鈕的持有對(duì)象,看是要統(tǒng)計(jì)cell的點(diǎn)擊事件坚弱,還是cell里面的控件的點(diǎn)擊事件蜀备。
propertyPath是屬性路徑,有些不同的層級(jí)有相同的屬性名字荒叶,比如self.age 和 self.person.age碾阁,如果把propertyPath的值設(shè)為 person/age,取值的時(shí)候就會(huì)按照指定路徑進(jìn)行取值些楣。

從配置表來看瓷蛙,所有的hook事件都不再是一個(gè)寫死的id,而是從配置表里面拿的數(shù)據(jù)戈毒,解決了問題2提到的,由于不同的pageid導(dǎo)致無法Hook的瓶頸横堡。這樣的配置表有2個(gè)好處埋市,一是可以自由配置想統(tǒng)計(jì)的頁(yè)面,二是可以動(dòng)態(tài)下發(fā)命贴,只要配置正確道宅,即使不發(fā)版也可以拿到線上版本的數(shù)據(jù)食听。

效果如下:

2018-12-12 15:04:18.373103+0800 ZPMStatisticsDemo[1435:199292] 
 事件唯一標(biāo)識(shí)為:SecondViewController 
  pageid === 4687,
  pagename === SecondView頁(yè)面展示,
  pagepara === {
    testKey0 = 30;
    testKey1 = "Hello World";
    testKey2 =     {
        key = 1;
    };
    testKey3 =     (
        a,
        b,
        2
    );
} 

有了這個(gè)配置表,頁(yè)面的In污茵、Out樱报,就可以通過Hook頁(yè)面的ViewAppear和ViewDisAppear來攔截埋點(diǎn)了,減少了大量重復(fù)埋點(diǎn)的時(shí)間泞当。但是我們還有一個(gè)問題迹蛤,頁(yè)面進(jìn)出先后順序問題。

頁(yè)面進(jìn)出順序

試想一個(gè)場(chǎng)景襟士,我在JD頁(yè)點(diǎn)擊投遞簡(jiǎn)歷按鈕盗飒,可視化埋點(diǎn)雖然記錄了點(diǎn)擊事件,我們做分析的時(shí)候陋桂,確無法得知用戶點(diǎn)擊這個(gè)投遞按鈕是通過什么方式進(jìn)來的逆趣,是從搜索結(jié)果頁(yè)進(jìn)入,還是首頁(yè)推薦嗜历,還是推送進(jìn)入的宣渗。如果要知道這樣的進(jìn)出先后,就需要進(jìn)行頁(yè)面的傳遞梨州。如果有一個(gè)方案痕囱,自動(dòng)記錄頁(yè)面的進(jìn)出堆棧順序,那么這個(gè)問題就迎刃而解了摊唇。

解決方案:
通過Hook viewWillAppear:方法來實(shí)現(xiàn)咐蝇,具體代碼如下:

- (void)analysis_viewWillAppear:(BOOL)animated
{
    [self analysis_viewWillAppear:animated];
    
    NSString *identifier = [NSString stringWithFormat:@"%@", [self class]];
    NSDictionary *dic = [[[ZPMDataContainer sharedInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier];
    NSString *pageInfo = [NSString stringWithFormat:@"%@, %@",[self getCurrentTimes], [self class]];
    if (dic) {
        NSString *pageid = dic[@"userDefined"][@"pageid"];
        pageInfo = [pageInfo stringByAppendingFormat:@" ,%@",pageid];
    }
    [[ZPM_IO_Queue sharedInstance].queueArray addObject:pageInfo];
    
    // 把頁(yè)面出現(xiàn)順序保存起來
    if ([ZPM_IO_Queue sharedInstance].queueArray.count > 10) { // 這里只存10個(gè)頁(yè)面隊(duì)列
        [[ZPM_IO_Queue sharedInstance].queueArray removeObjectAtIndex:0];
    }
    NSLog(@"queueArray:%@",[ZPM_IO_Queue sharedInstance].queueArray);
}

打印了一下日志:

2018-12-12 15:04:36.813738+0800 ZPMStatisticsDemo[1435:199292] queueArray:(
    "2018-12-12 15:04:12, UINavigationController",
    "2018-12-12 15:04:12, HomeViewController ,3156",
    "2018-12-12 15:04:18, SecondViewController ,4687",
    "2018-12-12 15:04:32, ThirdViewController",
    "2018-12-12 15:04:36, SecondViewController ,4687"
)

從日志里可以看出,進(jìn)出隊(duì)列包含時(shí)間巷查、類名有序、和pageid,(UINavigationController代表的是這些頁(yè)面都是Navigation類型的岛请,而ThirdViewController沒有pageid是因?yàn)榕渲帽砝餂]有配)旭寿。這樣每個(gè)點(diǎn)擊事件的來源就一目了然了。

動(dòng)態(tài)獲取自定義上報(bào)事件

試想這樣一個(gè)場(chǎng)景崇败,即使解決了動(dòng)態(tài)Pageid的問題盅称,如果統(tǒng)計(jì)需求,要在不同頁(yè)面拿不同的參數(shù)后室,比如簡(jiǎn)歷頁(yè)我要獲取簡(jiǎn)歷id缩膝、簡(jiǎn)歷編號(hào)、個(gè)人信息岸霹,JD頁(yè)我又要頁(yè)面數(shù)據(jù)疾层、各個(gè)點(diǎn)擊事件。每個(gè)頁(yè)面要拿的參數(shù)都是不一樣的贡避,那么豈不是即使解決了pageid不同的瓶頸痛黎,最后還是落得要手動(dòng)代碼埋點(diǎn)的下場(chǎng)予弧,因?yàn)槊總€(gè)頁(yè)面的上報(bào)的參數(shù)都是不一樣的。那這種情況下該如何解決呢湖饱?

一般來說有2種解決方案

第一種是代碼埋點(diǎn)掖蛤,對(duì)于高度自定義的上報(bào)就是得用代碼來埋點(diǎn),因?yàn)榧词故窍裆癫哌@樣的全埋點(diǎn)策略井厌,也無法做到所有地方的精確埋點(diǎn)蚓庭。

這里主要是介紹第二種方案,取參埋點(diǎn)法旗笔。簡(jiǎn)單介紹一下什么是取參埋點(diǎn)彪置,取參埋點(diǎn)其實(shí)就是利用Runtime,獲取一個(gè)類所有的property屬性蝇恶,即成員變量拳魁,比如搜索結(jié)果頁(yè)的列表數(shù)據(jù),是存放在一個(gè)叫l(wèi)istData的數(shù)組里的撮弧,那么通過Hook機(jī)制潘懊,動(dòng)態(tài)拿到listData,就可以拿到里面的數(shù)據(jù)進(jìn)行上傳操作贿衍。

這樣只需要通過配置表授舟,添加自己想獲取的屬性數(shù)據(jù),就能上報(bào)這樣的數(shù)據(jù)(前提是這個(gè)頁(yè)面有這樣的成員變量贸辈,局部變量的數(shù)據(jù)只能手動(dòng)埋點(diǎn)了)释树。

取參埋點(diǎn)的部分代碼:

+ (BOOL)getVariableWithClass:(Class) myClass varName:(NSString *)name
{
    unsigned int outCount, i;
    Ivar *ivars = class_copyIvarList(myClass, &outCount);
    for (i = 0; i < outCount; i++) {
        Ivar property = ivars[i];
        NSString *keyName = [NSString stringWithCString:ivar_getName(property) encoding:NSUTF8StringEncoding];
        keyName = [keyName stringByReplacingOccurrencesOfString:@"_" withString:@""];
        if ([keyName isEqualToString:name]) {
            return YES;
        }
    }
    return NO;
}

+ (id)captureVarforInstance:(id)instance varName:(NSString *)varName
{
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([instance class], &count);
    
    // 檢測(cè)是否存在這個(gè)屬性
    BOOL exit = [ZPMCapturePropertyTool getVariableWithClass:[instance class] varName:varName];
    
    id value = nil;
    if (exit) {
        value = [instance valueForKey:varName];
    }
    
    if (!value) {
        NSMutableArray *varNameArray = [NSMutableArray arrayWithCapacity:0];
        for (int i = 0; i < count; i++) {
            objc_property_t property = properties[i];
            NSString *propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];
            NSArray *splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""];
            if (splitPropertyAttributes.count < 2) {
                continue;
            }
            NSString *className = [splitPropertyAttributes objectAtIndex:1];
            Class cls = NSClassFromString(className);
            NSBundle *bundle2 = [NSBundle bundleForClass:cls];
            if (bundle2 == [NSBundle mainBundle]) {
                //                NSLog(@"自定義的類----- %@", className);
                const char *name = property_getName(property);
                NSString *varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding];
                [varNameArray addObject:varname];
            } else {
                //                NSLog(@"系統(tǒng)的類");
            }
        }
        
        for (NSString *name in varNameArray) {
            id newValue = [instance valueForKey:name];
            if (newValue) {
                value = [newValue valueForKey:varName];
                if (value) {
                    return value;
                }else{
                    value = [[self class] captureVarforInstance:newValue varName:varName];
                }
            }
        }
    }
    return value;
}

總結(jié)

以上討論的方案主要是解決了提出的4個(gè)問題,盡量可以減少代碼的侵入性擎淤,以及以后的維護(hù)成本奢啥。同時(shí)可以動(dòng)態(tài)更新埋點(diǎn)數(shù)據(jù),而不需要通過發(fā)版的方式解決嘴拢。但是以上方案也只是涵蓋了大部分場(chǎng)景桩盲, 并非所有場(chǎng)景都適用,具體大家可以根據(jù)業(yè)務(wù)情況來決定使用范圍席吴。如果有更好的方案獲取提議赌结,歡迎來騷擾。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末孝冒,一起剝皮案震驚了整個(gè)濱河市柬姚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌庄涡,老刑警劉巖量承,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宴合,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門迹鹅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卦洽,“玉大人,你說我怎么就攤上這事斜棚》У伲” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵弟蚀,是天一觀的道長(zhǎng)蚤霞。 經(jīng)常有香客問我,道長(zhǎng)义钉,這世上最難降的妖魔是什么昧绣? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮捶闸,結(jié)果婚禮上夜畴,老公的妹妹穿的比我還像新娘。我一直安慰自己删壮,他們只是感情好贪绘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著央碟,像睡著了一般税灌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亿虽,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天菱涤,我揣著相機(jī)與錄音,去河邊找鬼经柴。 笑死狸窘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坯认。 我是一名探鬼主播翻擒,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼牛哺!你這毒婦竟也來了陋气?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤引润,失蹤者是張志新(化名)和其女友劉穎巩趁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡议慰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蠢古,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片别凹。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡草讶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出炉菲,到底是詐尸還是另有隱情堕战,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布拍霜,位于F島的核電站嘱丢,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏祠饺。R本人自食惡果不足惜越驻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吠裆。 院中可真熱鬧伐谈,春花似錦、人聲如沸试疙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)祝旷。三九已至履澳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怀跛,已是汗流浹背距贷。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吻谋,地道東北人忠蝗。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像漓拾,于是被迫代替她去往敵國(guó)和親阁最。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • https://mp.weixin.qq.com/s/u-HmmrSAgtER1N2pKxCm0A 隨著公司業(yè)務(wù)的...
    海浪萌物閱讀 3,069評(píng)論 1 1
  • 前言 最近跟同事花了點(diǎn)時(shí)間來思考可視化埋點(diǎn)骇两,并沒有什么突破性的進(jìn)展速种,不過市面上很多關(guān)于可視化埋點(diǎn)的技術(shù)文章都在講達(dá)...
    daixunry閱讀 7,980評(píng)論 1 38
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 10,871評(píng)論 6 13
  • 寫在題前:文章為本人原創(chuàng), 如果文章轉(zhuǎn)載低千,必須標(biāo)明作者與出處配阵,并將原文鏈接以及github地址附在文章首行, 否則...
    SandyLoo閱讀 21,114評(píng)論 22 150
  • 【怦然心動(dòng)】20170617學(xué)習(xí)力踐行day94 蘋果妹把百數(shù)方格給翻出來了,因?yàn)槲疫€沒聽微課棋傍,所以一直藏著呢救拉。只...
    怦然心動(dòng)818閱讀 93評(píng)論 0 0