前言
隨著公司業(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)識來識別每一個(gè)事件, 針對指定的事件進(jìn)行取參埋點(diǎn)。而事件的標(biāo)識與參數(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ù)據(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)”的簡單實(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)如下:

從業(yè)務(wù)架構(gòu)上來看,可視化埋點(diǎn)主要對頁面Out、In(PV)、按鈕等事件點(diǎn)擊(Action)、列表的滑動(dòng)、點(diǎn)擊等(List)、手勢動(dòng)作(Gesture)進(jìn)行埋點(diǎn),就能覆蓋90%以上統(tǒng)計(jì)事件。
要解決的問題
代碼埋點(diǎn)可以解決所有的自定義埋點(diǎn),深入程度也是最高的,但是他有天然劣勢,就是每出現(xiàn)一個(gè)新的頁面,新的需求,都需要開發(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),特別是那些頁面的進(jìn)出、停留時(shí)長等的埋點(diǎn),重復(fù)的埋點(diǎn)徒增開發(fā)時(shí)間。
2、pageid(pagecode)不同而且無規(guī)律問題
雖然可視化埋點(diǎn)可以利用Hook原理,來解決這個(gè)問題,但由于統(tǒng)計(jì)的要求,每個(gè)頁面都自帶不同的pageid或者pagecode,這樣一來無法利用父類的方式去一次性埋點(diǎn),因?yàn)榧词雇ㄟ^繼承的方式,也無法做到每個(gè)子類都有不同的pageid。即使利用Hook原理,去Hook每一個(gè)頁面的Appear和DisAppear方法,也無法對這些不同的頁面注入不同的Pagecode,這樣唯一標(biāo)識又構(gòu)成了瓶頸。
3、動(dòng)態(tài)埋點(diǎn)問題
即使進(jìn)行了代碼埋點(diǎn),每個(gè)版本都進(jìn)行埋點(diǎn),但是卻無法對已經(jīng)上線的版本進(jìn)行埋點(diǎn),假如上線后有些埋點(diǎn)忘記埋了,就只能等到下個(gè)版本才能進(jìn)行埋點(diǎn)的添加,是否有辦法做到動(dòng)態(tài)的下發(fā)配置來對線上版本增加埋點(diǎn),而不需要發(fā)版呢?
4、頁面OI先后順序問題
代碼埋點(diǎn)的方式,如果想知道C頁面是從A頁面進(jìn)來的,還是從B頁面還是先A再B最后在進(jìn)入C的,就得對每個(gè)頁面進(jìn)行一個(gè)傳值,而且這樣做還有一個(gè)弊端就是可能不知道用戶這個(gè)C頁面可能是這樣的:A->B->D->B->A->B->C,普通的代碼埋點(diǎn)只能知道是B到C,卻不了解進(jìn)入C之前其實(shí)有很多前進(jìn)后退頁面的操作,這樣對數(shù)據(jù)分析可能就會(huì)有偏差,那是否有辦法做到自動(dòng)記錄頁面進(jìn)出的方案呢?
解決方案
唯一標(biāo)識的組成方式主要是又 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)擊事件。

但是剛才問題2提到,只是單純的Hook,無法解決PageCode、ActionCode如果埋入的問題,即:“事件唯一標(biāo)識符“如何埋入的問題。所以在這里,我們要利用一份配置表來管理這個(gè)“事件唯一標(biāo)識符“。
這里主要分為兩個(gè)部分 :
事件的鎖定
事件的鎖定主要是靠 “事件唯一標(biāo)識符”來鎖定,而事件的唯一標(biāo)識是由我們寫入配置表中的。這里分為兩種,本地配置表和線上下載的配置表。埋點(diǎn)數(shù)據(jù)的上報(bào)。
埋點(diǎn)數(shù)據(jù)的數(shù)據(jù)又分為兩種類型: 固定數(shù)據(jù)與可變的業(yè)務(wù)數(shù)據(jù), 而固定數(shù)據(jù)我們可以直接寫到配置表中,通過唯一標(biāo)識來獲取。而對于業(yè)務(wù)數(shù)據(jù),我是這么理解的:數(shù)據(jù)是有持有者的,例如我們Controller的一個(gè)屬性值,又或者數(shù)據(jù)再M(fèi)odel的某一個(gè)層級。這么的話我們就可以通過KVC的的方式來遞歸獲取該屬性的值來取到業(yè)務(wù)數(shù)據(jù)。
整體解決方案
由于業(yè)務(wù)中的事件場景是多樣的,以iOS為例,在此我以UIControl(Button、Switch、TextField等都屬于Control), UITablview(CollectionView與TableView基本相同,Android里對應(yīng)的則是ListView),UITapGesture,UIViewController的PV統(tǒng)計(jì)為例,介紹一下具體思路。
- UIViewController PV統(tǒng)計(jì)
頁面的統(tǒng)計(jì)較為簡單,利用Method Swizzing hook 系統(tǒng)的viewDidLoad, 直接通過頁面名稱即可鎖定頁面的展示代碼如下:
+ (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)識為:%@ \n pageid === %@,\n pagename === %@,\n pagepara === %@ \n", [self class], pageid, pagename, uploadDic);
}
}
- UIControl 點(diǎn)擊統(tǒng)計(jì)
主要通過hook sendAction:to:forEvent: 來實(shí)現(xiàn), 其唯一標(biāo)識符我們用 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);
}
}
TableView (CollectionView) 的點(diǎn)擊統(tǒng)計(jì)
tablview的唯一標(biāo)識, 我們使用 delegate.class/tableview.class/tableview.tag的組合來唯一鎖定。主要是通過hook setDelegate 方法, 在設(shè)置代理的時(shí)候再去交互 didSelect(點(diǎn)擊)、scrollViewDidScroll(滑動(dòng)列表)方法來實(shí)現(xiàn)。具體代碼先不上了。gesture方式添加的的點(diǎn)擊統(tǒng)計(jì)
gesture的事件,是通過 hook initWithTarget:action:方法來實(shí)現(xiàn)的, 事件的唯一標(biāo)識依然是target.class/actionname來鎖定的。
從上面的代碼可以看出,這個(gè)pageid和eventid都不是寫死的,而且從一個(gè)字典里面獲取值,如dic[@"userDefined"][@"pageid"],那么這個(gè)value從哪獲取呢,這里就要用到配置表了。
配置表結(jié)構(gòu)
配置表是一個(gè)json數(shù)據(jù)。 針對不同的場景 (UIControl , 頁面PV, Tabeview, Gesture)都做了區(qū)分, 用不同的key區(qū)別。 對于 "固定參數(shù)" , 我們之間寫到配置表中,而對于業(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è)頁面"
},
"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": "首頁展示了"
},
"pagePara": {
"testKey10": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"SecondViewController": {
"userDefined": {
"pageid": "4687",
"pagename": "SecondView頁面展示"
},
"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": "手勢1對應(yīng)的id",
"pagename": "手勢1對應(yīng)的page name"
},
"pagePara": {
"testKey1": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"ViewController/gesture2clicked:":{
"userDefined": {
"eventid": "201803074|93",
"target": "",
"pageid": "手勢2對應(yīng)的id",
"pagename": "手勢2對應(yīng)的page name"
},
"pagePara": {
"testKey2": {
"propertyName": "testPara",
"propertyPath":"",
"containIn": "0"
}
}
},
"SecondViewController/gesture3clicked:":{
"userDefined": {
"eventid": "201803074|98",
"target": "",
"pageid": "gesture3clicked",
"pagename": "手勢3對應(yīng)的page name"
},
"pagePara": {
"user_age": {
}
}
}
}
}
json最外層有四個(gè)Key, 分別為 ACTION PAGEPV TABLEVIEW GESTURE, 分別對應(yīng) UIControl的點(diǎn)擊,頁面PV,tableview cell點(diǎn)擊, Gesture 單擊事件的參數(shù)。 每個(gè)key對應(yīng)的value為json格式,Json中的keys, 即為唯一標(biāo)識符。
標(biāo)識符下的json有兩個(gè)key :
userDefine指的固定數(shù)據(jù),即直接取值進(jìn)行上報(bào)。
而pagePara為業(yè)務(wù)參數(shù)。pagePara對應(yīng)的value也是一個(gè)json, json的keys,即上報(bào)的keys,value內(nèi)的json包含三個(gè)參數(shù):
propertyName為屬性名字,
containIn 參數(shù)只有0 ,1 兩種情況,用來區(qū)分類似Tableview的Cell里面按鈕的持有對象,看是要統(tǒng)計(jì)cell的點(diǎn)擊事件,還是cell里面的控件的點(diǎn)擊事件。
propertyPath是屬性路徑,有些不同的層級有相同的屬性名字,比如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ì)的頁面,二是可以動(dòng)態(tài)下發(fā),只要配置正確,即使不發(fā)版也可以拿到線上版本的數(shù)據(jù)。
效果如下:
2018-12-12 15:04:18.373103+0800 ZPMStatisticsDemo[1435:199292]
事件唯一標(biāo)識為:SecondViewController
pageid === 4687,
pagename === SecondView頁面展示,
pagepara === {
testKey0 = 30;
testKey1 = "Hello World";
testKey2 = {
key = 1;
};
testKey3 = (
a,
b,
2
);
}
有了這個(gè)配置表,頁面的In、Out,就可以通過Hook頁面的ViewAppear和ViewDisAppear來攔截埋點(diǎn)了,減少了大量重復(fù)埋點(diǎn)的時(shí)間。但是我們還有一個(gè)問題,頁面進(jìn)出先后順序問題。
頁面進(jìn)出順序
試想一個(gè)場景,我在JD頁點(diǎn)擊投遞簡歷按鈕,可視化埋點(diǎn)雖然記錄了點(diǎn)擊事件,我們做分析的時(shí)候,確無法得知用戶點(diǎn)擊這個(gè)投遞按鈕是通過什么方式進(jìn)來的,是從搜索結(jié)果頁進(jìn)入,還是首頁推薦,還是推送進(jìn)入的。如果要知道這樣的進(jìn)出先后,就需要進(jìn)行頁面的傳遞。如果有一個(gè)方案,自動(dòng)記錄頁面的進(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];
// 把頁面出現(xiàn)順序保存起來
if ([ZPM_IO_Queue sharedInstance].queueArray.count > 10) { // 這里只存10個(gè)頁面隊(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代表的是這些頁面都是Navigation類型的,而ThirdViewController沒有pageid是因?yàn)榕渲帽砝餂]有配)。這樣每個(gè)點(diǎn)擊事件的來源就一目了然了。
動(dòng)態(tài)獲取自定義上報(bào)事件
試想這樣一個(gè)場景,即使解決了動(dòng)態(tài)Pageid的問題,如果統(tǒng)計(jì)需求,要在不同頁面拿不同的參數(shù),比如簡歷頁我要獲取簡歷id、簡歷編號、個(gè)人信息,JD頁我又要頁面數(shù)據(jù)、各個(gè)點(diǎn)擊事件。每個(gè)頁面要拿的參數(shù)都是不一樣的,那么豈不是即使解決了pageid不同的瓶頸,最后還是落得要手動(dòng)代碼埋點(diǎn)的下場,因?yàn)槊總€(gè)頁面的上報(bào)的參數(shù)都是不一樣的。那這種情況下該如何解決呢?
一般來說有2種解決方案
第一種是代碼埋點(diǎn),對于高度自定義的上報(bào)就是得用代碼來埋點(diǎn),因?yàn)榧词故窍裆癫哌@樣的全埋點(diǎn)策略,也無法做到所有地方的精確埋點(diǎn)。
這里主要是介紹第二種方案,取參埋點(diǎn)法。簡單介紹一下什么是取參埋點(diǎn),取參埋點(diǎn)其實(shí)就是利用Runtime,獲取一個(gè)類所有的property屬性,即成員變量,比如搜索結(jié)果頁的列表數(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è)頁面有這樣的成員變量,局部變量的數(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);
// 檢測是否存在這個(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ā)版的方式解決。但是以上方案也只是涵蓋了大部分場景, 并非所有場景都適用,具體大家可以根據(jù)業(yè)務(wù)情況來決定使用范圍。如果有更好的方案獲取提議,歡迎來騷擾。