iOS無埋點(diǎn)數(shù)據(jù)SDK實(shí)踐之路

本篇文章是基于 網(wǎng)易樂得無埋點(diǎn)數(shù)據(jù)SDK 總結(jié)而成诅岩。負(fù)責(zé)無埋點(diǎn)數(shù)據(jù)收集 SDK 的開發(fā)已經(jīng)有半年多了鞭盟,期間在組內(nèi)進(jìn)行過相關(guān)分享普办,現(xiàn)在覺得是時(shí)候拿出去和同行們交流下了砾肺。本篇主要講一下SDK的整體實(shí)現(xiàn)思路以及關(guān)鍵的技術(shù)點(diǎn)弛作。

SDK 已經(jīng)具備不需要代碼埋點(diǎn)就能 自動(dòng)的刽射、動(dòng)態(tài)可配的袜爪、全面且正確 的收集用戶在使用 App 時(shí)的所有事件數(shù)據(jù)炸裆。除此之外八秃,還單獨(dú)開發(fā)了與之配合的圈選SDK碱妆,能夠在 App 端完成對(duì)界面元素的圈配以及 KVC 配置的上傳。而界面元素圈配的工作完全可以交給用研與產(chǎn)品人員來做昔驱,減輕了開發(fā)人員的工作量疹尾。

SDK 已有的功能可以分為兩大部分:

  • 基本事件數(shù)據(jù)的收集:基本事件的收集是指應(yīng)用冷啟動(dòng)事件、頁面事件骤肛、用戶點(diǎn)擊事件纳本、ScrollView滑動(dòng)事件等,這部分全部都是自動(dòng)完成的腋颠,實(shí)現(xiàn)思路會(huì)在第一節(jié)中介紹繁成。
  • 業(yè)務(wù)層數(shù)據(jù)的收集:業(yè)務(wù)層數(shù)據(jù)的收集是指對(duì)與業(yè)務(wù)功能相關(guān)的一些數(shù)據(jù),例如:在用戶點(diǎn)擊提交訂單按鈕時(shí)淑玫,收集用戶購買的物品以及訂單總金額的數(shù)據(jù)巾腕。這種業(yè)務(wù)層數(shù)據(jù)的收集以往大多通過 代碼埋點(diǎn) 的方式去做面睛,本SDK則真正的實(shí)現(xiàn)了 無埋點(diǎn) 的去獲取這些想要的業(yè)務(wù)數(shù)據(jù)。這部分的實(shí)現(xiàn)會(huì)在本文的第二節(jié)詳細(xì)介紹祠墅。

SDK的整體實(shí)現(xiàn)思路

SDK 整體采用了 AOP(Aspect-Oriented-Programming)即面向切面編程的思想侮穿,就是動(dòng)態(tài)的在函數(shù)調(diào)用的前后插入數(shù)據(jù)收集的代碼。在 Objective-C 中的實(shí)現(xiàn)是基于 Runtime 特性的 Method Swizzling 黑魔法毁嗦。

SDK 的數(shù)據(jù)收集功能的實(shí)現(xiàn)主要通過 Method Swizzlinghook 相應(yīng)的方法亲茅。hook的方法大致可以分為3類:系統(tǒng)類的方法、系統(tǒng)類的Delegate方法狗准、自定義類的方法克锣。

系統(tǒng)類的方法

系統(tǒng)類的方法是指系統(tǒng)框架中提供的基礎(chǔ)類的方法,如 UIApplication腔长、UIViewController 等袭祟。SDK 在實(shí)現(xiàn)某些功能時(shí),需要hook這些類的方法捞附。例如在實(shí)現(xiàn)對(duì)頁面事件的收集時(shí)巾乳,主要hookUIViewController 的生命周期的方法:viewDidLoadviewDidAppear鸟召、viewDidDisappear胆绊、dealloc

系統(tǒng)類的 Delegate 方法

系統(tǒng)類的 Delegate 方法主要指 UIKit 框架中提供的 Delegate 中的方法,如 UIScrollViewDelegate欧募、UITableViewDelegate压状、UIWebViewDelegate 等。SDK 中的大多數(shù)功能都是通過hook這些協(xié)議中的方法來完成的跟继。例如在實(shí)現(xiàn)列表元素點(diǎn)擊事件的收集時(shí)种冬,主要 hookUITableViewDelegate 中的 tableView:didSelectRowAtIndexPath: 方法。

自定義類的方法

顧名思義舔糖,自定義類的方法是指開發(fā)人員在工程中自已定義的類娱两,而非系統(tǒng)類的方法。SDK的一些功能是通過hook 這些類的方法來實(shí)現(xiàn)金吗。例如在SDK實(shí)現(xiàn)對(duì)手勢操作的事件收集時(shí)谷婆,需要hook手勢對(duì)象所指定的target 中的 action 方法,而 target 通常都是自定義類辽聊。其實(shí)hook系統(tǒng)類的 delegate 方法也可以看成是 hook 自定義類的方法,因?yàn)橄到y(tǒng)類的 delegate 方法大多都是需要在自定義類中實(shí)現(xiàn)期贫。

這部分看起來是借助于 AOP 來添加數(shù)據(jù)收集的代碼跟匆,但是在真正做的時(shí)候,也并沒有想的那么簡單通砍,涉及到很多細(xì)節(jié)上的問題玛臂,例如:如何將導(dǎo)航欄與系統(tǒng)彈窗的點(diǎn)擊事件歸屬到合適頁面中烤蜕、如何區(qū)分UIControlEventValueChanged事件、如何解決hook手勢操作引起的性能問題等等迹冤。不過這部分內(nèi)容并不是本篇文章的重點(diǎn)讽营,因此這里不打算多說,之后會(huì)單獨(dú)寫一篇文章來講述遇到的一些坑泡徙。

SDK的關(guān)鍵技術(shù)的實(shí)現(xiàn)

viewPath 及 viewId 的生成及優(yōu)化

為了對(duì) APP 中某個(gè)頁面的某個(gè) view 進(jìn)行數(shù)據(jù)收集橱鹏、統(tǒng)計(jì)與分析,首先就需要能夠唯一的標(biāo)識(shí)與定位這個(gè)視圖堪藐,這可以說是數(shù)據(jù)收集 SDK 的一個(gè)重要前提莉兰。那么怎樣去唯一的標(biāo)識(shí) APP 中的某個(gè) view 呢?SDK 中使用了 viewPathviewId 來完成礁竞。

1. viewPath 的組成

其實(shí)整個(gè) APP 的視圖結(jié)構(gòu)可以看成是一顆樹(viewTree)糖荒,樹的根節(jié)點(diǎn)就是 UIWindow,樹的枝干由UIViewControllerUIView組成模捂,樹的葉節(jié)點(diǎn)都是由UIView組成捶朵。

那么在viewTree中用什么信息來表示其中任意一個(gè) view 的位置呢?很容易想到的就是使用目標(biāo) view 到根之間的每個(gè)節(jié)點(diǎn)的深度(層次)組成一個(gè)路徑狂男,而節(jié)點(diǎn)的深度(層次)是指此節(jié)點(diǎn)在父節(jié)點(diǎn)中的 index综看。這樣確實(shí)能夠唯一的表示此 view 了,但是有一個(gè)缺點(diǎn):它的可讀性很差并淋。因此在此基礎(chǔ)上又增加了每個(gè)節(jié)點(diǎn)的名稱寓搬,節(jié)點(diǎn)的名稱由當(dāng)前節(jié)點(diǎn)的 view 的類名來表示。

因此县耽,在 viewTree 中句喷,由一個(gè) view 到根節(jié)點(diǎn)之間的每個(gè)節(jié)點(diǎn)的名稱與深度(層次)共同組成的信息構(gòu)成了此 view 的viewPath。另外兔毙,由于在做 view 的統(tǒng)計(jì)分析時(shí)唾琼,都是以頁面為單位的,因此 SDK 在生成 viewPath 時(shí)澎剥,只到 view 所在的 UIViewController 級(jí)別锡溯,而非根部的 UIWindow。這樣做也在一定程度上減少了viewPath 的長度哑姚。

2. UITableViewCell/UICollectionCell 的深度表示

在 App 開發(fā)中祭饭,最常用而且最重要的控件就是UITableViewUICollectionView。針對(duì)這種可復(fù)用視圖叙量,里面會(huì)包含很多 Cell倡蝙,而且 Cell 個(gè)數(shù)也不確定,那么里面的每一個(gè) Cell 應(yīng)該怎么去表示其深度呢绞佩?答案是indexPath寺鸥。雖然每個(gè) Cell 都可能被復(fù)用猪钮,但是不同的 Cell 都對(duì)應(yīng)一個(gè)唯一的indexPath,因此完全可以使用indexPath值來表示其深度胆建。

3. viewPath 的表示形式與示例

我們已經(jīng)知道烤低,viewPath就是由各節(jié)點(diǎn)的類名與深度組成,那么接下來就使用這些信息來表示出 viewPath笆载。下面結(jié)合一個(gè)具體的示例來簡單說一下扑馁,我隨便從項(xiàng)目中找了一個(gè):

路徑中各個(gè)節(jié)點(diǎn)的類名是:

HYGHallSlideViewController-UIScrollView-HYGHallProductTableView-UITableViewWrapperView-HYGHallProductCell-UITableViewCellContentView-HYGHallProductView。

路徑中各個(gè)節(jié)點(diǎn)的深度是:0-0-1-0-0:2-0-1

接下來就是將這兩者放到一起來構(gòu)成 viewPath宰译,SDK 的表示方式如下:

viewPath:HYGHallSlideViewController-UIScrollView-HYGHallProductTableView-UITableViewWrapperView-HYGHallProductCell-UITableViewCellContentView-HYGHallProductView & 0-0-1-0-0:2-0-1

其實(shí)就是使用 & 連接符簡單的拼接到一起檐蚜。這樣做可以方便將兩者組合與分離開,便于后面的viewPath匹配沿侈。另外闯第,網(wǎng)上還有一種類似于 xPath 的表示方式:

HYGHallSlideViewController[0]/UIScrollView[0]/HYGHallProductTableView[1]/UITableViewWrapperView[0]/HYGHallProductCell[0:2]/UITableViewCellContentView[0]/HYGHallProductView[1]

不過個(gè)人覺得xPath的方式稍微復(fù)雜了點(diǎn),在組合以及拆分上都相對(duì)麻煩些缀拭。不過話說回來咳短,viewPath的形式是次要的,大家可以按照各自喜歡的方式去表示就行蛛淋,無須糾結(jié)于哪種形式更好咙好。

4.針對(duì) viewPath 的優(yōu)化

4.1 優(yōu)化節(jié)點(diǎn)的深度的計(jì)算方式

上面提到在計(jì)算各節(jié)點(diǎn)的深度時(shí),是采用當(dāng)前 view 位于其父 view 中的所有子 view 中的 index 值褐荷。不過在實(shí)際的開發(fā)中勾效,viewTree 有時(shí)候會(huì)根據(jù)用戶的操作有所變動(dòng)。仍然舉個(gè)栗子:

  • 假設(shè)一個(gè) UIView 中有三個(gè)子 view叛甫,先后加入的順序是:label层宫、button1、button2其监,按照之前的計(jì)算方式萌腿,這 3 個(gè)子 view 的深度依次是:0、1抖苦、2毁菱。這時(shí)候用戶點(diǎn)擊了一個(gè)按鈕,label1 從父 view 中被移除了锌历。此時(shí) UIView 只有 2 個(gè)子view:button1贮庞、button2,而且深度變?yōu)榱耍?究西、1窗慎。如圖所示:

可以看出僅僅由于其中一個(gè)子view 被移除,卻導(dǎo)致其它子 view 的深度都發(fā)生了變化怔揩。因此捉邢,SDK 為了在新增/移除某一 view 時(shí),盡量減少對(duì)已有 view 的深度的影響商膊,調(diào)整了對(duì)節(jié)點(diǎn)的深度的計(jì)算方式:采用當(dāng)前 view 位于其父 view 中的所有 同類型 子 view 中的index 值伏伐。

我們再看一下上面的這個(gè)例子,最初 label晕拆、button1藐翎、button2 的深度依次是:0、0实幕、1吝镣。在 label 被移除后,button1昆庇、button2 的深度依次為:0末贾、1≌海可以看出拱撵,在這個(gè)例子中,label 的移除并未對(duì) button1表蝙、button2 的深度造成影響拴测,這種調(diào)整后的計(jì)算方式在一定程度上增強(qiáng)了 viewPath 的抗干擾性。

另外府蛇,調(diào)整后的深度的計(jì)算方式是依賴于各節(jié)點(diǎn)的類型的集索,因此,此時(shí)必須要將各節(jié)點(diǎn)的名稱放到viewPath中汇跨,而不再是僅僅為了增加可讀性务荆。

4.2 viewPath 針對(duì) Swift 的優(yōu)化

眾所周知,Swift文件在獲取其類名時(shí)扰法,會(huì)自動(dòng)添加此文件所在的Module名前綴:如果Swift文件在主工程中蛹含,則會(huì)添加工程的名字;如果是在某個(gè)組件中塞颁,并且項(xiàng)目開啟了 use frameworks! 選項(xiàng)浦箱,則會(huì)添加組件的名字§袈啵總的來說酷窥,在含有swift 的項(xiàng)目中(包括純 swift/OC 與 swift 混編),viewPath中會(huì)包含各 Swift 文件的ModuleName伴网,那么在如下情況下:

  • 某個(gè) OC 文件被使用 Swift 重寫了
  • 某個(gè) Swift 文件被從主工程移至某個(gè)組件庫中蓬推,或者從組件庫移至主工程中
  • 主工程在引用組件庫時(shí),在開啟與關(guān)閉use frameworks!之間進(jìn)行切換

上述3種情況下澡腾,文件的類名都會(huì)由于ModuleName而發(fā)生變化沸伏,進(jìn)而會(huì)導(dǎo)致 viewPath 的改變糕珊,工程文件在結(jié)構(gòu)上的調(diào)整都可能會(huì)直接對(duì)viewPath造成影響。

實(shí)際開發(fā)中毅糟,特別是對(duì)于較老的OC項(xiàng)目红选,經(jīng)常會(huì)對(duì)項(xiàng)目的OC文件使用Swift重寫。因此 SDK 有必要去避免viewPath因?yàn)檫@類情況而發(fā)生變化姆另。

其實(shí)這個(gè)問題的解決方案很簡單喇肋,既然是由于類名中的ModuleName前綴的改變造成的,那么就干脆在生成viewPath時(shí)迹辐,去掉所有的SwiftModuleName前綴蝶防。這種做法能夠解決對(duì)viewPath的影響,但是細(xì)心的人可能會(huì)意識(shí)到另一個(gè)隱藏的問題:如果在不同的組件庫中明吩,兩個(gè)不同的視圖或控制器具有相同的名字(在Swift中是允許的间学,因?yàn)橛?code>Module進(jìn)行區(qū)分),這種情況下贺喝,viewPath是否存在無法區(qū)分的情況菱鸥?

其實(shí)經(jīng)過仔細(xì)考慮,這個(gè)擔(dān)憂有點(diǎn)多余躏鱼,因?yàn)榫退銉蓚€(gè)Module中的視圖或控制器名字一樣氮采,但是他們里面的視圖結(jié)構(gòu)會(huì)有所不同,進(jìn)而深度也不一樣染苛,viewPath也不會(huì)完全相同鹊漠。

4.3 在包含子VC時(shí),優(yōu)化VC的深度的計(jì)算

前面提到茶行,viewPath只表示到距離 view 最近的一個(gè) VC躯概,VC 的深度的計(jì)算也是此 VC 的 view 所在的父 view 的所有子 view 中的深度。在實(shí)際的 iOS 開發(fā)中畔师,可能會(huì)經(jīng)常使用addChildViewController:添加多個(gè)子 VC 來實(shí)現(xiàn)復(fù)雜的頁面娶靡,但是在包含子 VC 時(shí),VC 的深度計(jì)算就有可能會(huì)存在問題看锉。還是舉一個(gè)簡單的栗子:

  • 假設(shè)一個(gè) containerVC 中包含4個(gè)子VC:VC1姿锭、VC2、VC3伯铣、VC4呻此。在每個(gè)子VC首次被展示時(shí),子VC會(huì)先被add進(jìn)來腔寡,而子 VC 的 view 也會(huì)被 add 到一個(gè)scrollView 上焚鲜。這時(shí)候這幾個(gè)子VC首次的查看順序的不同將會(huì)導(dǎo)致它們的深度的變化:如果查看順序是:VC1、VC2、VC3忿磅、VC4糯彬,那么它們的深度依次為:VC1(0)、VC2(1)葱她、VC3(2)情连、VC4(3);如果查看順序是:VC3览效、VC1、VC4虫几、VC2锤灿,深度則變成了:VC1(1)、VC2(3)辆脸、VC3(0)但校、VC4(2)。這種情況導(dǎo)致 viewPath 不可靠且無法保證唯一性啡氢。

SDK 為了解決上述情況状囱,調(diào)整了 VC 的深度的計(jì)算:不再采用其 view 的深度,而是直接使用固定的0倘是。因?yàn)?VC 已經(jīng)是viewPath的根級(jí)別了亭枷,它的深度信息已經(jīng)不重要了。

不過這種方案會(huì)引起另一個(gè)小問題搀崭,如果上述子 VC 的 VC1 和 VC2 是同一個(gè)類的不同實(shí)例叨粘,那么他們內(nèi)部的視圖結(jié)構(gòu)是完全一樣的,這時(shí)候如果使用固定的 VC 深度(0)瘤睹,通過viewPath就無法區(qū)分具體是哪個(gè)子 VC 的 view 了升敲。針對(duì)這種同一類的不同實(shí)例,如果想進(jìn)一步區(qū)分它們轰传,SDK 采用了另一個(gè)方案:頁面別名驴党。

5. viewId 的生成

viewPath 已經(jīng)能夠唯一標(biāo)識(shí)某個(gè) view 了,為何還需要viewId呢获茬?其實(shí)主要原因是:viewPath 的長度不固定港庄,而且一般都會(huì)比較長,不便于后臺(tái)使用它作為 view 的唯一標(biāo)識(shí)锦茁。因此 SDK 使用viewPath信息通過MD5加密生成一個(gè)固定長度的值作為viewId攘轩。

6. viewPath 與 viewId 重復(fù)時(shí)的解決方案

經(jīng)過對(duì)viewPath的優(yōu)化,SDK 已經(jīng)盡可能的保證了viewPath的穩(wěn)定性码俩。但是并不表示只依靠viewPath就能區(qū)分所有的點(diǎn)擊事件度帮。有時(shí)同一個(gè)viewPath的 view 具有不同的表現(xiàn)形式與作用,例如下面的情況:

  • 同一個(gè)按鈕在不同的狀態(tài)下,顯示不同的文字笨篷。例如:一個(gè)按鈕在未添加商品前顯示“添加”瞳秽;添加了商品之后,立刻顯示成“清除”
  • 同一個(gè)view上具有多處點(diǎn)擊事件率翅,例如 SegmentControl练俐、UISwitchUIStepper

上面的這2種情況冕臭,都是同一個(gè)viewPath對(duì)應(yīng)多個(gè)事件腺晾,此時(shí)如果只使用viewPath無法區(qū)分出不同的狀態(tài)或事件。

針對(duì)這類問題辜贵,SDK 的解決方案是:viewPath + “其它信息” 悯蝉。這里的 “其它信息” 是視不同情況而定的,比如: 在上面的情況1中托慨,“其它信息” 就是按鈕的 title鼻由。在情況2中,“其它信息” 是 SegmentControl 的 selectedIndex 和 UISwitch 的 isOn 屬性的值厚棵。SDK 在進(jìn)行數(shù)據(jù)收集時(shí)蕉世,會(huì)上傳 view 的這些信息,再結(jié)合圈選SDK就能讓后臺(tái)在做統(tǒng)計(jì)時(shí)區(qū)分出這些不同的事件了婆硬。

關(guān)于“其它信息”狠轻,再補(bǔ)充一點(diǎn),除了 SDK 事先知道要獲取的信息之外彬犯,還有一類就是業(yè)務(wù)數(shù)據(jù)哈误。例如:有一個(gè)商品列表頁,每一行顯示一個(gè)商品躏嚎,如果后臺(tái)想統(tǒng)計(jì)的不是列表中每一行的點(diǎn)擊蜜自,而是每個(gè)商品的點(diǎn)擊,那么此時(shí)的“其它信息”就應(yīng)該是productId 了卢佣。關(guān)于 SDK 對(duì)業(yè)務(wù)層數(shù)據(jù)的獲取與上報(bào)請看下面的介紹重荠。

SDK無埋點(diǎn)業(yè)務(wù)數(shù)據(jù)收集的實(shí)現(xiàn)

講完了 viewPath 之后,接下來詳細(xì)介紹下 SDK 的另一個(gè)關(guān)鍵技術(shù):基于 viewPathKVC 實(shí)現(xiàn) SDK 的無埋點(diǎn)業(yè)務(wù)數(shù)據(jù)收集功能虚茶。首先戈鲁,先簡單分析一下傳統(tǒng)的 代碼埋點(diǎn) 存在的缺點(diǎn),大致有以下幾個(gè):

  • 埋點(diǎn)代碼與業(yè)務(wù)邏輯代碼混合在一起嘹叫,增加了代碼的維護(hù)成本婆殿;
  • 埋點(diǎn)代碼需要跟隨APP版本一起發(fā)布,耽誤數(shù)據(jù)的收集與統(tǒng)計(jì)罩扇;
  • 埋點(diǎn)時(shí)存在錯(cuò)埋婆芦、漏埋等情況怕磨,無法動(dòng)態(tài)更新及添加;

為了解決上述的 代碼埋點(diǎn) 的缺陷消约,SDK 實(shí)現(xiàn)了真正意義上的 無埋點(diǎn) 來對(duì)業(yè)務(wù)數(shù)據(jù)進(jìn)行收集肠鲫。

1. 無埋點(diǎn)的實(shí)現(xiàn)架構(gòu)

SDK 的無埋點(diǎn)功能的實(shí)現(xiàn)主要依賴于 viewPathKVCviewPath前面已經(jīng)介紹了或粮,它主要用于標(biāo)識(shí)viewTree中的某個(gè) view导饲。而KVC對(duì)于 iOS 開發(fā)者也不陌生,堪稱 iOS 開發(fā)中的黑魔法之一氯材。通過KVC我們能夠通過 key 或 keyPath 直接訪問對(duì)象的屬性渣锦,而不需要調(diào)用明確的存取方法。關(guān)于KVC如果不太了解氢哮,請自行學(xué)習(xí)泡挺,這里不再過多闡述。

那么如何實(shí)現(xiàn)不需要代碼埋點(diǎn)就能隨意獲取想要的業(yè)務(wù)數(shù)據(jù)呢命浴?先看一下 SDK 的無埋點(diǎn)技術(shù)的整體架構(gòu)圖:

從上圖可以看出,在實(shí)現(xiàn) SDK 的無埋點(diǎn)數(shù)據(jù)收集時(shí)贱除,主要分為3步:上傳KVC配置生闲、請求KVC配置、業(yè)務(wù)數(shù)據(jù)的收集與上報(bào)月幌。

2. 什么是 KVC 配置

在上圖中出現(xiàn)了 KVC配置碍讯,那么下面先簡單介紹下什么是KVC配置。其實(shí) KVC配置 就是一些用來描述 App 應(yīng)該在什么時(shí)機(jī)去收集什么數(shù)據(jù)的信息扯躺,包含的主要信息有:

  • appKey:用來標(biāo)識(shí)是哪個(gè)應(yīng)用
  • appVersion:用來標(biāo)識(shí)應(yīng)用的版本號(hào)
  • viewEvent:標(biāo)識(shí)某個(gè)事件類型(收集時(shí)機(jī))捉兴,例如:ButtonClick、ListItemClick录语、ViewTap等
  • viewPath:目標(biāo) view 在viewTree中的信息
  • keyPath:目標(biāo) view 與要收集的業(yè)務(wù)數(shù)據(jù)間的關(guān)聯(lián)路徑倍啥,用于KVC取值
  • keyName:為要收集的業(yè)務(wù)數(shù)據(jù)定義一個(gè)key,最終組成 key-value 的形式上報(bào)澎埠。用于區(qū)分多個(gè)收集的數(shù)據(jù)

3. KVC配置的上傳與下發(fā)

  • 上傳KVC配置

    • 利用 圈選SDK 上傳 KVC配置 的操作對(duì)于用戶是透明的虽缕,主要由開發(fā)人員進(jìn)行上傳與管理。此操作可以在任何時(shí)候進(jìn)行蒲稳,在想要收集某個(gè)或某些版本的 App 中的業(yè)務(wù)數(shù)據(jù)時(shí)氮趋,上傳相應(yīng)的KVC配置信息至后臺(tái)即可,達(dá)到了根據(jù)需要?jiǎng)討B(tài)可配的效果江耀。
  • 請求KVC配置

    • SDK 在初始化時(shí)會(huì)觸發(fā) KVC配置 的請求操作剩胁,從后臺(tái)拉取 App 當(dāng)前版本對(duì)應(yīng)的所有KVC配置,并將請求結(jié)果緩存起來祥国,以提供給下一步使用昵观。

4. 業(yè)務(wù)數(shù)據(jù)的收集與上報(bào)

這一部分是 SDK 無埋點(diǎn)技術(shù)的核心,接下來詳細(xì)介紹這部分的實(shí)現(xiàn)邏輯。它的實(shí)現(xiàn)流程如下:

這個(gè)環(huán)節(jié)的核心是基于viewPath的 view 匹配索昂,主要實(shí)現(xiàn)是通過循環(huán)遍歷viewPath的每個(gè)節(jié)點(diǎn)的信息與當(dāng)前 view 及其父view 依次進(jìn)行匹配建车。因此這一步會(huì)產(chǎn)生一定的時(shí)間與性能消耗。為了盡可能減少這部分的操作椒惨,SDK 中使用了一些方式進(jìn)行優(yōu)化缤至,其中一個(gè)就是基于緩存view的優(yōu)化。

4.1 基于緩存view的優(yōu)化

SDK 采用緩存上一次匹配成功的 view 信息的方式康谆,來減少一些不必要的viewPath匹配操作领斥。這里主要緩存的 view 信息有:

  • targetView:上一次通過viewPath匹配成功的 view 對(duì)象。
  • indexPath: 上一次通過viewPath匹配成功的 view 的indexPath沃暗,如果沒有則為nil月洛。
1. viewEvent 匹配

第一步先進(jìn)行事件類型的匹配。如果KVC配置信息指定的 viewEvent 是 ButtonClick孽锥,那么可以輕松的過濾掉 ListItemClick嚼黔、ViewTap 等其它事件。這一步能夠過濾一大部分事件惜辑,只有事件類型匹配成功才繼續(xù)進(jìn)行下一步唬涧。

2. targetView 匹配

接下來就是將緩存的 targetView 與當(dāng)前 view 進(jìn)行比較。如果兩者指向同一對(duì)象盛撑,則進(jìn)行第3步碎节,否則直接進(jìn)入第4步

3. indexPath 匹配

有人可能不明白為何要添加這一步呢?其實(shí)這一步也很重要抵卫,是對(duì)第2步的補(bǔ)充狮荔,主要是用來處理 Cell 可復(fù)用性的情況。

如果第2步中緩存的 targetView 是 Cell 或 Cell 中的某個(gè) subview介粘,那么第2步的匹配成功殖氏,并不能保證當(dāng)前 view 就是我們真正想匹配的 view。這個(gè)可能不太容易理解姻采,還是舉個(gè)簡單的例子來說明一下:

  • 假如一個(gè) Cell 中有一個(gè) button受葛,在第1行的 button 被點(diǎn)擊時(shí),通過viewPath匹配成功了偎谁,那么這時(shí) targetView 緩存了第1行的 button 對(duì)象总滩。接下來向下滑動(dòng)列表,第一行被劃出屏幕巡雨,第10行劃入屏幕闰渔,同時(shí)第10行復(fù)用了第1行的 Cell,這時(shí)再點(diǎn)擊 button 去匹配時(shí)铐望,由于 Cell 復(fù)用的原因冈涧,targetView 與當(dāng)前 button 肯定指向同一個(gè)對(duì)象茂附,但是卻不是我們真正想匹配的第1行的 button《焦可以看出:在有 Cell 復(fù)用的情況下营曼,無法確定第2步的結(jié)果一定正確。

因此愚隧,在第2步的基礎(chǔ)上又增加了indexPath匹配蒂阱。indexPath的匹配邏輯為:如果緩存的indexPath不為nil并且與當(dāng)前view的indexPath不相等,則進(jìn)入第4步狂塘;否則表明當(dāng)前的 view 就是上次剛剛匹配成功的录煤,也就沒必要進(jìn)行viewPath匹配,可以直接進(jìn)入第5步荞胡。

4. viewPath 匹配

這一步就是對(duì)當(dāng)前的 view 及其父view 與KVC配置中的viewPath的各個(gè)節(jié)點(diǎn)進(jìn)行逐個(gè)匹配妈踊。由于是一個(gè)循環(huán)操作,因此會(huì)有一定的時(shí)間消耗泪漂,其實(shí)在這部分的匹配中廊营,也做了一些簡單的優(yōu)化。在真正進(jìn)入循環(huán)匹配之前萝勤,先進(jìn)行如下3步判斷:

  • 判斷 view 類名是否相等露筒;
  • 判斷 view 所在的 viewController 類名是否相等;
  • 判斷 view 所在的 window 類名是否相等纵刘;

上述的3個(gè)判斷也能過濾很多不必要的匹配。只有這3個(gè)判斷均通過后荸哟,才進(jìn)行viewPath循環(huán)匹配假哎。

5. KVC 取值與上報(bào)

到了這一步,就已經(jīng)驗(yàn)證了數(shù)據(jù)收集的時(shí)機(jī)是正確的鞍历。接下來就可以直接使用 KVC配置信息中的keyPath調(diào)用 valueForKeyPath: 方法獲取對(duì)應(yīng)的值舵抹。如果值不為nil,就與 keyName 組成一個(gè)鍵值對(duì)劣砍,放到當(dāng)前的事件數(shù)據(jù)中一起上報(bào)上去惧蛹。這樣后臺(tái)就可以通過key去查找到相應(yīng)的業(yè)務(wù)數(shù)據(jù)了。

上面只是簡要介紹了一下匹配時(shí)的邏輯刑枝,在實(shí)際開發(fā)中還會(huì)添加對(duì) cell 的indexPath通配的情況的處理香嗓,由于文章篇幅這里不再詳細(xì)講解。

5. 增加對(duì) KVC 的異常處理

SDK 的無埋點(diǎn)功能的實(shí)現(xiàn)其實(shí)主要依賴于KVC装畅,但是眾所周知靠娱,KVC是非常危險(xiǎn)的,很容易造成程序崩潰掠兄。例如一旦 key 或 keyPath 所對(duì)應(yīng)的屬性名不存在像云,立刻會(huì)導(dǎo)致程序拋出一個(gè)NSUndefinedKeyException異常锌雀,如果應(yīng)用沒有處理此異常,程序就會(huì)Crash迅诬。

因此腋逆,為了避免程序Crash,SDK 內(nèi)部增加了對(duì)KVC異常的處理侈贷。具體實(shí)現(xiàn)是給 NSObject 增加一個(gè) Category 惩歉,重寫 valueForUndefinedKey: 方法,并在方法中return nil铐维。

@implementation NSObject (KVCExceptionHandler)
- (nullable id)valueForUndefinedKey:(NSString *)key
{
    return nil;
}
@end

其它關(guān)鍵技術(shù)

當(dāng)然柬泽,SDK 的實(shí)現(xiàn)中還有很多關(guān)鍵技術(shù)點(diǎn),比如:SDK 對(duì) RN 頁面的數(shù)據(jù)收集嫁蛇、頁面別名方案的實(shí)現(xiàn)锨并、Method SwizzlingAspects的兼容等。由于本文的篇幅已經(jīng)很長了睬棚,而且考慮到大家讀文章的耐性都不會(huì)太長第煮,所以這里就先不講解了,后續(xù)會(huì)再寫文章單獨(dú)介紹抑党。

END

文章寫了這么多包警,其實(shí)主要介紹了 SDK 中的兩個(gè)關(guān)鍵技術(shù)點(diǎn),希望對(duì)你們能有一些參考價(jià)值底靠。另外害晦,如果有人對(duì)本文的方案有更好的建議,歡迎一起討論學(xué)習(xí)暑中。

最后壹瘟,要特別感謝我的同事王佳樂,由于他對(duì)文章的排版與校對(duì)工作鳄逾,才使得本文能更好的展示給大家稻轨。同時(shí)也要感謝組內(nèi)的所有同事,在我開發(fā)遇到困難時(shí)雕凹,給予了我很多的幫助。

Q & A

關(guān)于對(duì)本文內(nèi)容提出的一些問題枚抵,將全部記錄在這里(簡書評(píng)論里的除外),并進(jìn)行統(tǒng)一解答询筏。

Q1: SDK 都使用KVC配置獲取業(yè)務(wù)數(shù)據(jù),是否會(huì)增加維護(hù)KVC配置的工作竖慧?

A1: 會(huì)有對(duì) KVC配置 的維護(hù)與管理工作逆屡,不過 SDK 也簡化了這塊的管理工作踱讨。

一般來說魏蔗,上傳的所有的 KVC配置 需要與 App 的版本相對(duì)應(yīng),因?yàn)?App 版本不同會(huì)直接導(dǎo)致keyPath可能不一樣痹筛。所以與 KVC配置 相關(guān)的工作有如下2個(gè):

  1. 針對(duì)當(dāng)前 App 版本上傳相應(yīng)的 KVC配置,以獲取想要的業(yè)務(wù)數(shù)據(jù)
  2. 當(dāng) App 新版本發(fā)布時(shí)谣旁,需要對(duì)之前版本上的 KVC配置 逐一驗(yàn)證滋早,是否仍然適用于新版本杆麸。如果仍然適用,則直接在管理后臺(tái)上把新的版本號(hào)添加到此 KVC配置饼问;如果不再適用揭斧,則對(duì)新版本再上傳一個(gè)新的KVC配置。

從上面可以看出盅视,在 App 版本不斷迭代的過程中左冬,KVC配置 會(huì)越來越多纸型,相應(yīng)的維護(hù)與管理工作也相當(dāng)繁瑣梅忌。

為了解決這個(gè)痛點(diǎn)牧氮,SDK 中增加了一種方案來避免這種重復(fù)且繁瑣的工作。具體的方案是:

  • 在上傳 KVC 配置時(shí)丹莲,指定某個(gè)區(qū)間的版本,或者不指定具體的版本(即應(yīng)用到當(dāng)前所有版本上)盯另;
  • SDK 在使用KVC配置獲取業(yè)務(wù)數(shù)據(jù)失敗時(shí)洲赵,添加相關(guān)的錯(cuò)誤日志叠萍,并上報(bào)上去。其中錯(cuò)誤日志里包含了appKey辅鲸、appVersion瓢湃、keyPath等信息赫蛇,這樣就能在后臺(tái)清晰的看到哪些 KVC配置 在哪個(gè) App 版本上存在問題悟耘;
  • 使用腳本監(jiān)控與KVC相關(guān)的錯(cuò)誤日志。如果監(jiān)控到有錯(cuò)誤日志上報(bào)筏勒,則發(fā)送郵件通知給相關(guān)人員旺嬉;

因此邪媳,SDK 采用此方案優(yōu)化之后,KVC配置 的管理工作就只有1個(gè)了:

  • 根據(jù)Log信息快速找到對(duì)應(yīng)的 KVC配置迅涮,并上傳一個(gè)針對(duì)新版本的 KVC配置

Q2: 對(duì)于 “內(nèi)容與位置” 可能會(huì)隨時(shí)間而變動(dòng)時(shí)叮姑,如何實(shí)現(xiàn)數(shù)據(jù)收集與統(tǒng)計(jì)据悔?

A2: 使用圈選SDK與數(shù)據(jù)SDK共同完成動(dòng)態(tài)數(shù)據(jù)的收集與統(tǒng)計(jì)

這個(gè)問題在實(shí)際產(chǎn)品中也比較常見,比如 App 首頁的內(nèi)容大多是通過后臺(tái)配置的耕拷。
這個(gè)問題其實(shí)可以轉(zhuǎn)化或分解成如下的2個(gè)情況:

  • 同一位置會(huì)顯示不同的內(nèi)容
  • 同一內(nèi)容會(huì)顯示在不同的位置

注意托享,這2個(gè)并非同一個(gè)闰围,它們分別對(duì)應(yīng)于不同的場景砂吞,同時(shí)數(shù)據(jù)收集的方案也有所不同。

另外校仑,“位置” 可以是在列表中迄沫,也可以是非列表中的,不過這個(gè)對(duì)整體的方案沒有太大影響泰佳,僅僅是在不關(guān)心位置時(shí)viewPath中的通配符位置不同尘吗。

A2.1 同一位置顯示不同的內(nèi)容

例子:在 App 首頁有一個(gè)展示最近活動(dòng)的位置睬捶,先展示活動(dòng)1的圖片,過一段時(shí)間運(yùn)營人員又配成活動(dòng)2的圖片臀晃。如何統(tǒng)計(jì)活動(dòng)1积仗、活動(dòng)2各自的點(diǎn)擊量蜕猫?

針對(duì)這種場景回右,SDK 的解決方案是:“關(guān)心位置” + “關(guān)心內(nèi)容”
“關(guān)心位置” 的意思是只使用當(dāng)前的位置渺氧,具體表現(xiàn)是viewPath中不包含任何通配符侣背;“關(guān)心內(nèi)容” 的意思是指定一個(gè)想要統(tǒng)計(jì)的內(nèi)容慨默。

整個(gè)過程可以分解為如下3個(gè)環(huán)節(jié):

  • 圈選SDK上傳“關(guān)心位置”的KVC配置厦取。KVC配置中指定獲取活動(dòng)的urlkeyPath
  • 數(shù)據(jù)SDK在活動(dòng)發(fā)生點(diǎn)擊時(shí)铡买,收集當(dāng)前活動(dòng)對(duì)應(yīng)的url奇钞,并跟隨點(diǎn)擊事件一起上報(bào)漂坏。
  • 圈選SDK上傳“關(guān)心位置” + “關(guān)心內(nèi)容”的圈選配置樊拓,關(guān)心的內(nèi)容指定為想要統(tǒng)計(jì)的活動(dòng)的url值筋夏。
A2.2 同一內(nèi)容顯示在不同的位置

例子:App 首頁有4個(gè)固定的入口,假設(shè)其中一個(gè)叫“熱門推薦”骗随,那么根據(jù)后臺(tái)配置的順序不同鸿染,“熱門推薦”可能被顯示在4個(gè)位置中的任何1個(gè)乞巧,即一段時(shí)間顯示在第1個(gè),過一段時(shí)間可能顯示在第2個(gè)位置蚕冬。這時(shí)如何統(tǒng)計(jì)出“熱門推薦”的點(diǎn)擊量囤热?

針對(duì)這種場景旁蔼,SDK 的解決方案是:“不關(guān)心位置” + “關(guān)心內(nèi)容”
“不關(guān)心位置” 是指viewPath中含有通配符蚓炬,用于表示viewTree中的多個(gè)位置肯夏。例如想要匹配列表所有行時(shí)犀暑,則將viewPath中的indexPath替換為通配符耐亏。

這個(gè)問題的解決過程也分為如下3步:

  • 圈選SDK上傳“不關(guān)心位置”的KVC配置。KVC配置中指定獲取入口的 title 的keyPath暇矫。
  • 數(shù)據(jù)SDK在4個(gè)中任何一個(gè)入口被點(diǎn)擊時(shí)李根,都去收集入口的 title几睛,并跟隨點(diǎn)擊事件一起上報(bào)所森。
  • 圈選SDK上傳“不關(guān)心位置” + “關(guān)心內(nèi)容”的圈選配置焕济,關(guān)心的內(nèi)容指定為“熱門推薦”。

到這里掩幢,數(shù)據(jù)收集與圈選配置的工作都已經(jīng)做完了粒蜈,接下來就是后臺(tái)的數(shù)據(jù)統(tǒng)計(jì)了旗国。
上述2種情況對(duì)后臺(tái)進(jìn)行統(tǒng)計(jì)沒有區(qū)別能曾,都使用一個(gè)統(tǒng)計(jì)方案,這里也介紹一下后臺(tái)大概的統(tǒng)計(jì)思路:

  • 拿到第3步中上傳的圈選配置寿冕,根據(jù)viewPath“關(guān)心的內(nèi)容” 生成一個(gè)正則表達(dá)式驼唱,然后從數(shù)據(jù) SDK 上報(bào)的原始數(shù)據(jù)中進(jìn)行正則匹配玫恳,進(jìn)而統(tǒng)計(jì)出相應(yīng)數(shù)據(jù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惭婿,隨后出現(xiàn)的幾起案子财饥,更是在濱河造成了極大的恐慌钥星,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,207評(píng)論 6 521
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異乖篷,居然都是意外死亡透且,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,455評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門琳骡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來楣号,“玉大人炫狱,你說我怎么就攤上這事剔猿」榫矗” “怎么了?”我有些...
    開封第一講書人閱讀 170,031評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長陆爽。 經(jīng)常有香客問我慌闭,道長驴剔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,334評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮布讹,結(jié)果婚禮上描验,老公的妹妹穿的比我還像新娘膘流。我一直安慰自己鲁沥,他們只是感情好画恰,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,322評(píng)論 6 398
  • 文/花漫 我一把揭開白布允扇。 她就那樣靜靜地躺著蔼两,像睡著了一般逞度。 火紅的嫁衣襯著肌膚如雪档泽。 梳的紋絲不亂的頭發(fā)上馆匿,一...
    開封第一講書人閱讀 52,895評(píng)論 1 314
  • 那天渐北,我揣著相機(jī)與錄音铭拧,去河邊找鬼搀菩。 笑死肪跋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谜洽。 我是一名探鬼主播褥琐,決...
    沈念sama閱讀 41,300評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼敌呈,長吁一口氣:“原來是場噩夢啊……” “哼磕洪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鲫咽,我...
    開封第一講書人閱讀 40,264評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤分尸,失蹤者是張志新(化名)和其女友劉穎箩绍,沒想到半個(gè)月后材蛛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卑吭,經(jīng)...
    沈念sama閱讀 46,784評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豆赏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,870評(píng)論 3 343
  • 正文 我和宋清朗相戀三年掷邦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耙饰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纹份。...
    茶點(diǎn)故事閱讀 40,989評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡件已,死狀恐怖元暴,靈堂內(nèi)的尸體忽然破棺而出茉盏,到底是詐尸還是另有隱情,我是刑警寧澤铜秆,帶...
    沈念sama閱讀 36,649評(píng)論 5 351
  • 正文 年R本政府宣布核蘸,位于F島的核電站啸驯,受9級(jí)特大地震影響罚斗,放射性物質(zhì)發(fā)生泄漏惰聂。R本人自食惡果不足惜咱筛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,331評(píng)論 3 336
  • 文/蒙蒙 一溉愁、第九天 我趴在偏房一處隱蔽的房頂上張望饲趋。 院中可真熱鬧奕塑,春花似錦龄砰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,814評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽你弦。三九已至禽作,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彻磁,已是汗流浹背衷蜓。 一陣腳步聲響...
    開封第一講書人閱讀 33,940評(píng)論 1 275
  • 我被黑心中介騙來泰國打工磁浇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留朽褪,地道東北人置吓。 一個(gè)月前我還...
    沈念sama閱讀 49,452評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像缔赠,于是被迫代替她去往敵國和親衍锚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,995評(píng)論 2 361

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