本篇文章是基于 網(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 Swizzling
來 hook
相應(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í)巾乳,主要hook
了 UIViewController
的生命周期的方法:viewDidLoad
、viewDidAppear
鸟召、viewDidDisappear
胆绊、dealloc
系統(tǒng)類的 Delegate 方法
系統(tǒng)類的 Delegate 方法主要指 UIKit 框架中提供的 Delegate 中的方法,如 UIScrollViewDelegate欧募、UITableViewDelegate压状、UIWebViewDelegate 等。SDK 中的大多數(shù)功能都是通過hook
這些協(xié)議中的方法來完成的跟继。例如在實(shí)現(xiàn)列表元素點(diǎn)擊事件的收集時(shí)种冬,主要 hook
了 UITableViewDelegate
中的 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 中使用了 viewPath
與 viewId
來完成礁竞。
1. viewPath 的組成
其實(shí)整個(gè) APP 的視圖結(jié)構(gòu)可以看成是一顆樹(viewTree
)糖荒,樹的根節(jié)點(diǎn)就是 UIWindow,樹的枝干由UIViewController
及UIView
組成模捂,樹的葉節(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ā)中祭饭,最常用而且最重要的控件就是UITableView
與UICollectionView
。針對(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í)迹辐,去掉所有的Swift
的ModuleName
前綴蝶防。這種做法能夠解決對(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练俐、
UISwitch
、UIStepper
等
上面的這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ù):基于 viewPath
與 KVC
實(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)主要依賴于 viewPath
與 KVC
。viewPath
前面已經(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 Swizzling
與Aspects
的兼容等。由于本文的篇幅已經(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è):
- 針對(duì)當(dāng)前 App 版本上傳相應(yīng)的 KVC配置,以獲取想要的業(yè)務(wù)數(shù)據(jù)
- 當(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)的
url
的keyPath
。 - 數(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ù)。