1. 程式人生 > >iOS無埋點資料SDK實踐之路

iOS無埋點資料SDK實踐之路

SDK 已經具備不需要程式碼埋點就能 自動的、動態可配的、全面且正確 的收集使用者在使用 App 時的所有事件資料。除此之外,還單獨開發了與之配合的圈選SDK,能夠在 App 端完成對介面元素的圈配以及 KVC 配置的上傳。而介面元素圈配的工作完全可以交給用研與產品人員來做,減輕了開發人員的工作量。
SDK 已有的功能可以分為兩大部分:

基本事件資料的收集:基本事件的收集是指應用冷啟動事件、頁面事件、使用者點選事件、ScrollView滑動事件等,這部分全部都是自動完成的,實現思路會在第一節中介紹。

業務層資料的收集:業務層資料的收集是指對與業務功能相關的一些資料,例如:在使用者點選提交訂單按鈕時,收集使用者購買的物品以及訂單總金額的資料。這種業務層資料的收集以往大多通過 程式碼埋點 的方式去做,本SDK則真正的實現了 無埋點 的去獲取這些想要的業務資料。這部分的實現會在本文的第二節詳細介紹。

SDK的整體實現思路
SDK 整體採用了 AOP(Aspect-Oriented-Programming)即面向切面程式設計的思想,就是動態的在函式呼叫的前後插入資料收集的程式碼。在 Objective-C 中的實現是基於 Runtime 特性的 Method Swizzling 黑魔法。
SDK 的資料收集功能的實現主要通過 Method Swizzling 來 hook 相應的方法。hook的方法大致可以分為3類:系統類的方法、系統類的Delegate方法、自定義類的方法。
系統類的方法
系統類的方法是指系統框架中提供的基礎類的方法,如 UIApplication、UIViewController 等。SDK 在實現某些功能時,需要hook這些類的方法。例如在實現對頁面事件的收集時,主要hook了 UIViewController 的生命週期的方法:viewDidLoad、viewDidAppear、viewDidDisappear、dealloc
系統類的 Delegate 方法
系統類的 Delegate 方法主要指 UIKit 框架中提供的 Delegate 中的方法,如 UIScrollViewDelegate、UITableViewDelegate、UIWebViewDelegate 等。SDK 中的大多數功能都是通過hook這些協議中的方法來完成的。例如在實現列表元素點選事件的收集時,主要 hook 了 UITableViewDelegate 中的 tableView:didSelectRowAtIndexPath: 方法。
自定義類的方法
顧名思義,自定義類的方法是指開發人員在工程中自已定義的類,而非系統類的方法。SDK的一些功能是通過hook 這些類的方法來實現。例如在SDK實現對手勢操作的事件收集時,需要hook手勢物件所指定的target 中的 action 方法,而 target 通常都是自定義類。其實hook系統類的 delegate 方法也可以看成是 hook 自定義類的方法,因為系統類的 delegate 方法大多都是需要在自定義類中實現。
這部分看起來是藉助於 AOP 來新增資料收集的程式碼,但是在真正做的時候,也並沒有想的那麼簡單,涉及到很多細節上的問題,例如:如何將導航欄與系統彈窗的點選事件歸屬到合適頁面中、如何區分UIControlEventValueChanged事件、如何解決hook手勢操作引起的效能問題等等。不過這部分內容並不是本篇文章的重點,因此這裡不打算多說,之後會單獨寫一篇文章來講述遇到的一些坑。
SDK的關鍵技術的實現
viewPath 及 viewId 的生成及優化
為了對 APP 中某個頁面的某個 view 進行資料收集、統計與分析,首先就需要能夠唯一的標識與定位這個檢視,這可以說是資料收集 SDK 的一個重要前提。那麼怎樣去唯一的標識 APP 中的某個 view 呢?SDK 中使用了 viewPath 與 viewId 來完成。

  1. viewPath 的組成
    其實整個 APP 的檢視結構可以看成是一顆樹(viewTree),樹的根節點就是 UIWindow,樹的枝幹由UIViewController及UIView組成,樹的葉節點都是由UIView組成。
    那麼在viewTree中用什麼資訊來表示其中任意一個 view 的位置呢?很容易想到的就是使用目標 view 到根之間的每個節點的深度(層次)組成一個路徑,而節點的深度(層次)是指此節點在父節點中的 index。這樣確實能夠唯一的表示此 view 了,但是有一個缺點:它的可讀性很差。因此在此基礎上又增加了每個節點的名稱,節點的名稱由當前節點的 view 的類名來表示。
    因此,在 viewTree 中,由一個 view 到根節點之間的每個節點的名稱與深度(層次)共同組成的資訊構成了此 view 的viewPath。另外,由於在做 view 的統計分析時,都是以頁面為單位的,因此 SDK 在生成 viewPath 時,只到 view 所在的 UIViewController 級別,而非根部的 UIWindow。這樣做也在一定程度上減少了viewPath 的長度。
  2. UITableViewCell/UICollectionCell 的深度表示
    在 App 開發中,最常用而且最重要的控制元件就是UITableView與UICollectionView。針對這種可複用檢視,裡面會包含很多 Cell,而且 Cell 個數也不確定,那麼裡面的每一個 Cell 應該怎麼去表示其深度呢?答案是indexPath。雖然每個 Cell 都可能被複用,但是不同的 Cell 都對應一個唯一的indexPath,因此完全可以使用indexPath值來表示其深度。
  3. viewPath 的表示形式與示例
    我們已經知道,viewPath就是由各節點的類名與深度組成,那麼接下來就使用這些資訊來表示出 viewPath。下面結合一個具體的示例來簡單說一下,我隨便從專案中找了一個:
    路徑中各個節點的類名是:

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

路徑中各個節點的深度是:0-0-1-0-0:2-0-1
接下來就是將這兩者放到一起來構成 viewPath,SDK 的表示方式如下:

viewPath:HYGHallSlideViewController-UIScrollView-HYGHallProductTableView-UITableViewWrapperVi

其實就是使用 & 連線符簡單的拼接到一起。這樣做可以方便將兩者組合與分離開,便於後面的viewPath匹配。另外,網上還有一種類似於 xPath 的表示方式:

HYGHallSlideViewController[0]/UIScrollView[0]/HYGHallProductTableView[1]/UITableViewWrapper

不過個人覺得xPath的方式稍微複雜了點,在組合以及拆分上都相對麻煩些。不過話說回來,viewPath的形式是次要的,大家可以按照各自喜歡的方式去表示就行,無須糾結於哪種形式更好。
4.針對 viewPath 的優化
4.1 優化節點的深度的計算方式
上面提到在計算各節點的深度時,是採用當前 view 位於其父 view 中的所有子 view 中的 index 值。不過在實際的開發中,viewTree 有時候會根據使用者的操作有所變動。仍然舉個栗子:

假設一個 UIView 中有三個子 view,先後加入的順序是:label、button1、button2,按照之前的計算方式,這 3 個子 view 的深度依次是:0、1、2。這時候使用者點選了一個按鈕,label1 從父 view 中被移除了。此時 UIView 只有 2 個子view:button1、button2,而且深度變為了:0、1。如圖所示:

iOS無埋點資料SDK實踐之路
可以看出僅僅由於其中一個子view 被移除,卻導致其它子 view 的深度都發生了變化。因此,SDK 為了在新增/移除某一 view 時,儘量減少對已有 view 的深度的影響,調整了對節點的深度的計算方式:採用當前 view 位於其父 view 中的所有 同類型 子 view 中的index 值。
我們再看一下上面的這個例子,最初 label、button1、button2 的深度依次是:0、0、1。在 label 被移除後,button1、button2 的深度依次為:0、1。可以看出,在這個例子中,label 的移除並未對 button1、button2 的深度造成影響,這種調整後的計算方式在一定程度上增強了 viewPath 的抗干擾性。
另外,調整後的深度的計算方式是依賴於各節點的型別的,因此,此時必須要將各節點的名稱放到viewPath中,而不再是僅僅為了增加可讀性。
4.2 viewPath 針對 Swift 的優化
眾所周知,Swift檔案在獲取其類名時,會自動新增此檔案所在的Module名字首:如果Swift檔案在主工程中,則會新增工程的名字;如果是在某個元件中,並且專案開啟了 use frameworks! 選項,則會新增元件的名字。總的來說,在含有swift 的專案中(包括純 swift/OC 與 swift 混編),viewPath中會包含各 Swift 檔案的ModuleName,那麼在如下情況下:

某個 OC 檔案被使用 Swift 重寫了
某個 Swift 檔案被從主工程移至某個元件庫中,或者從元件庫移至主工程中
主工程在引用元件庫時,在開啟與關閉use frameworks!之間進行切換

上述3種情況下,檔案的類名都會由於ModuleName而發生變化,進而會導致 viewPath 的改變,工程檔案在結構上的調整都可能會直接對viewPath造成影響。
實際開發中,特別是對於較老的OC專案,經常會對專案的OC檔案使用Swift重寫。因此 SDK 有必要去避免viewPath因為這類情況而發生變化。
其實這個問題的解決方案很簡單,既然是由於類名中的ModuleName字首的改變造成的,那麼就乾脆在生成viewPath時,去掉所有的Swift的ModuleName字首。這種做法能夠解決對viewPath的影響,但是細心的人可能會意識到另一個隱藏的問題:如果在不同的元件庫中,兩個不同的檢視或控制器具有相同的名字(在Swift中是允許的,因為有Module進行區分),這種情況下,viewPath是否存在無法區分的情況?
其實經過仔細考慮,這個擔憂有點多餘,因為就算兩個Module中的檢視或控制器名字一樣,但是他們裡面的檢視結構會有所不同,進而深度也不一樣,viewPath也不會完全相同。
4.3 在包含子VC時,優化VC的深度的計算
前面提到,viewPath只表示到距離 view 最近的一個 VC,VC 的深度的計算也是此 VC 的 view 所在的父 view 的所有子 view 中的深度。在實際的 iOS 開發中,可能會經常使用addChildViewController:新增多個子 VC 來實現複雜的頁面,但是在包含子 VC 時,VC 的深度計算就有可能會存在問題。還是舉一個簡單的栗子:

假設一個 containerVC 中包含4個子VC:VC1、VC2、VC3、VC4。在每個子VC首次被展示時,子VC會先被add進來,而子 VC 的 view 也會被 add 到一個scrollView 上。這時候這幾個子VC首次的檢視順序的不同將會導致它們的深度的變化:如果檢視順序是:VC1、VC2、VC3、VC4,那麼它們的深度依次為:VC1(0)、VC2(1)、VC3(2)、VC4(3);如果檢視順序是:VC3、VC1、VC4、VC2,深度則變成了:VC1(1)、VC2(3)、VC3(0)、VC4(2)。這種情況導致 viewPath 不可靠且無法保證唯一性。

SDK 為了解決上述情況,調整了 VC 的深度的計算:不再採用其 view 的深度,而是直接使用固定的0。因為 VC 已經是viewPath的根級別了,它的深度資訊已經不重要了。
不過這種方案會引起另一個小問題,如果上述子 VC 的 VC1 和 VC2 是同一個類的不同例項,那麼他們內部的檢視結構是完全一樣的,這時候如果使用固定的 VC 深度(0),通過viewPath就無法區分具體是哪個子 VC 的 view 了。針對這種同一類的不同例項,如果想進一步區分它們,SDK 採用了另一個方案:頁面別名。

  1. viewId 的生成
    viewPath 已經能夠唯一標識某個 view 了,為何還需要viewId呢?其實主要原因是:viewPath 的長度不固定,而且一般都會比較長,不便於後臺使用它作為 view 的唯一標識。因此 SDK 使用viewPath資訊通過MD5加密生成一個固定長度的值作為viewId。
  2. viewPath 與 viewId 重複時的解決方案
    經過對viewPath的優化,SDK 已經儘可能的保證了viewPath的穩定性。但是並不表示只依靠viewPath就能區分所有的點選事件。有時同一個viewPath的 view 具有不同的表現形式與作用,例如下面的情況:

同一個按鈕在不同的狀態下,顯示不同的文字。例如:一個按鈕在未新增商品前顯示“新增”;添加了商品之後,立刻顯示成“清除”
同一個view上具有多處點選事件,例如 SegmentControl、UISwitch、UIStepper等

上面的這2種情況,都是同一個viewPath對應多個事件,此時如果只使用viewPath無法區分出不同的狀態或事件。
針對這類問題,SDK 的解決方案是:viewPath + “其它資訊” 。這裡的 “其它資訊” 是視不同情況而定的,比如: 在上面的情況1中,“其它資訊” 就是按鈕的 title。在情況2中,“其它資訊” 是 SegmentControl 的 selectedIndex 和 UISwitch 的 isOn 屬性的值。SDK 在進行資料收集時,會上傳 view 的這些資訊,再結合圈選SDK就能讓後臺在做統計時區分出這些不同的事件了。
關於“其它資訊”,再補充一點,除了 SDK 事先知道要獲取的資訊之外,還有一類就是業務資料。例如:有一個商品列表頁,每一行顯示一個商品,如果後臺想統計的不是列表中每一行的點選,而是每個商品的點選,那麼此時的“其它資訊”就應該是productId 了。關於 SDK 對業務層資料的獲取與上報請看下面的介紹。
SDK無埋點業務資料收集的實現
講完了 viewPath 之後,接下來詳細介紹下 SDK 的另一個關鍵技術:基於 viewPath 與 KVC 實現 SDK 的無埋點業務資料收集功能。首先,先簡單分析一下傳統的 程式碼埋點 存在的缺點,大致有以下幾個:

埋點程式碼與業務邏輯程式碼混合在一起,增加了程式碼的維護成本;
埋點程式碼需要跟隨APP版本一起釋出,耽誤資料的收集與統計;
埋點時存在錯埋、漏埋等情況,無法動態更新及新增;

為了解決上述的 程式碼埋點 的缺陷,SDK 實現了真正意義上的 無埋點 來對業務資料進行收集。

  1. 無埋點的實現架構
    SDK 的無埋點功能的實現主要依賴於 viewPath 與 KVC。viewPath前面已經介紹了,它主要用於標識viewTree中的某個 view。而KVC對於 iOS 開發者也不陌生,堪稱 iOS 開發中的黑魔法之一。通過KVC我們能夠通過 key 或 keyPath 直接訪問物件的屬性,而不需要呼叫明確的存取方法。關於KVC如果不太瞭解,請自行學習,這裡不再過多闡述。
    那麼如何實現不需要程式碼埋點就能隨意獲取想要的業務資料呢?先看一下 SDK 的無埋點技術的整體架構圖:

iOS無埋點資料SDK實踐之路

從上圖可以看出,在實現 SDK 的無埋點資料收集時,主要分為3步:上傳KVC配置、請求KVC配置、業務資料的收集與上報。

  1. 什麼是 KVC 配置
    在上圖中出現了 KVC配置,那麼下面先簡單介紹下什麼是KVC配置。其實 KVC配置 就是一些用來描述 App 應該在什麼時機去收集什麼資料的資訊,包含的主要資訊有:

appKey:用來標識是哪個應用

appVersion:用來標識應用的版本號

viewEvent:標識某個事件型別(收集時機),例如:ButtonClick、ListItemClick、ViewTap等

viewPath:目標 view 在viewTree中的資訊

keyPath:目標 view 與要收集的業務資料間的關聯路徑,用於KVC取值

keyName:為要收集的業務資料定義一個key,最終組成 key-value 的形式上報。用於區分多個收集的資料

  1. KVC配置的上傳與下發

上傳KVC配置

利用 圈選SDK 上傳 KVC配置 的操作對於使用者是透明的,主要由開發人員進行上傳與管理。此操作可以在任何時候進行,在想要收集某個或某些版本的 App 中的業務資料時,上傳相應的KVC配置資訊至後臺即可,達到了根據需要動態可配的效果。

請求KVC配置

SDK 在初始化時會觸發 KVC配置 的請求操作,從後臺拉取 App 當前版本對應的所有KVC配置,並將請求結果快取起來,以提供給下一步使用。

  1. 業務資料的收集與上報
    這一部分是 SDK 無埋點技術的核心,接下來詳細介紹這部分的實現邏輯。它的實現流程如下:
    iOS無埋點資料SDK實踐之路
    這個環節的核心是基於viewPath的 view 匹配,主要實現是通過迴圈遍歷viewPath的每個節點的資訊與當前 view 及其父view 依次進行匹配。因此這一步會產生一定的時間與效能消耗。為了儘可能減少這部分的操作,SDK 中使用了一些方式進行優化,其中一個就是基於快取view的優化。
    4.1 基於快取view的優化
    SDK 採用快取上一次匹配成功的 view 資訊的方式,來減少一些不必要的viewPath匹配操作。這裡主要快取的 view 資訊有:

targetView:上一次通過viewPath匹配成功的 view 物件。
indexPath: 上一次通過viewPath匹配成功的 view 的indexPath,如果沒有則為nil。

  1. viewEvent 匹配
    第一步先進行事件型別的匹配。如果KVC配置資訊指定的 viewEvent 是 ButtonClick,那麼可以輕鬆的過濾掉 ListItemClick、ViewTap 等其它事件。這一步能夠過濾一大部分事件,只有事件型別匹配成功才繼續進行下一步。
  2. targetView 匹配
    接下來就是將快取的 targetView 與當前 view 進行比較。如果兩者指向同一物件,則進行第3步,否則直接進入第4步
  3. indexPath 匹配
    有人可能不明白為何要新增這一步呢?其實這一步也很重要,是對第2步的補充,主要是用來處理 Cell 可複用性的情況。
    如果第2步中快取的 targetView 是 Cell 或 Cell 中的某個 subview,那麼第2步的匹配成功,並不能保證當前 view 就是我們真正想匹配的 view。這個可能不太容易理解,還是舉個簡單的例子來說明一下:

假如一個 Cell 中有一個 button,在第1行的 button 被點選時,通過viewPath匹配成功了,那麼這時 targetView 快取了第1行的 button 物件。接下來向下滑動列表,第一行被劃出螢幕,第10行劃入螢幕,同時第10行復用了第1行的 Cell,這時再點選 button 去匹配時,由於 Cell 複用的原因,targetView 與當前 button 肯定指向同一個物件,但是卻不是我們真正想匹配的第1行的 button。可以看出:在有 Cell 複用的情況下,無法確定第2步的結果一定正確。

因此,在第2步的基礎上又增加了indexPath匹配。indexPath的匹配邏輯為:如果快取的indexPath不為nil並且與當前view的indexPath不相等,則進入第4步;否則表明當前的 view 就是上次剛剛匹配成功的,也就沒必要進行viewPath匹配,可以直接進入第5步。

  1. viewPath 匹配
    這一步就是對當前的 view 及其父view 與KVC配置中的viewPath的各個節點進行逐個匹配。由於是一個迴圈操作,因此會有一定的時間消耗,其實在這部分的匹配中,也做了一些簡單的優化。在真正進入迴圈匹配之前,先進行如下3步判斷:

判斷 view 類名是否相等;
判斷 view 所在的 viewController 類名是否相等;
判斷 view 所在的 window 類名是否相等;

上述的3個判斷也能過濾很多不必要的匹配。只有這3個判斷均通過後,才進行viewPath迴圈匹配。

  1. KVC 取值與上報
    到了這一步,就已經驗證了資料收集的時機是正確的。接下來就可以直接使用 KVC配置資訊中的keyPath呼叫 valueForKeyPath: 方法獲取對應的值。如果值不為nil,就與 keyName 組成一個鍵值對,放到當前的事件資料中一起上報上去。這樣後臺就可以通過key去查詢到相應的業務資料了。
    上面只是簡要介紹了一下匹配時的邏輯,在實際開發中還會新增對 cell 的indexPath通配的情況的處理,由於文章篇幅這裡不再詳細講解。
  2. 增加對 KVC 的異常處理
    SDK 的無埋點功能的實現其實主要依賴於KVC,但是眾所周知,KVC是非常危險的,很容易造成程式崩潰。例如一旦 key 或 keyPath 所對應的屬性名不存在,立刻會導致程式丟擲一個NSUndefinedKeyException異常,如果應用沒有處理此異常,程式就會Crash。
    因此,為了避免程式Crash,SDK 內部增加了對KVC異常的處理。具體實現是給 NSObject 增加一個 Category ,重寫 valueForUndefinedKey: 方法,並在方法中return nil。
@implementation NSObject (KVCExceptionHandler)
- (nullable id)valueForUndefinedKey:(NSString *)key
{
    return nil;
}
@end

其它關鍵技術
當然,SDK 的實現中還有很多關鍵技術點,比如:SDK 對 RN 頁面的資料收集、頁面別名方案的實現、Method Swizzling與Aspects的相容等。由於本文的篇幅已經很長了,而且考慮到大家讀文章的耐性都不會太長,所以這裡就先不講解了,後續會再寫文章單獨介紹。
END
文章寫了這麼多,其實主要介紹了 SDK 中的兩個關鍵技術點,希望對你們能有一些參考價值。另外,如果有人對本文的方案有更好的建議,歡迎一起討論學習。
最後,要特別感謝我的同事王佳樂,由於他對文章的排版與校對工作,才使得本文能更好的展示給大家。同時也要感謝組內的所有同事,在我開發遇到困難時,給予了我很多的幫助。
Q & A
關於對本文內容提出的一些問題,將全部記錄在這裡(簡書評論裡的除外),並進行統一解答。
Q1: SDK 都使用KVC配置獲取業務資料,是否會增加維護KVC配置的工作?
A1: 會有對 KVC配置 的維護與管理工作,不過 SDK 也簡化了這塊的管理工作。
一般來說,上傳的所有的 KVC配置 需要與 App 的版本相對應,因為 App 版本不同會直接導致keyPath可能不一樣。所以與 KVC配置 相關的工作有如下2個:

針對當前 App 版本上傳相應的 KVC配置,以獲取想要的業務資料
當 App 新版本釋出時,需要對之前版本上的 KVC配置 逐一驗證,是否仍然適用於新版本。如果仍然適用,則直接在管理後臺上把新的版本號新增到此 KVC配置;如果不再適用,則對新版本再上傳一個新的KVC配置。

從上面可以看出,在 App 版本不斷迭代的過程中,KVC配置 會越來越多,相應的維護與管理工作也相當繁瑣。
為了解決這個痛點,SDK 中增加了一種方案來避免這種重複且繁瑣的工作。具體的方案是:

在上傳 KVC 配置時,指定某個區間的版本,或者不指定具體的版本(即應用到當前所有版本上);
SDK 在使用KVC配置獲取業務資料失敗時,新增相關的錯誤日誌,並上報上去。其中錯誤日誌裡包含了appKey、appVersion、keyPath等資訊,這樣就能在後臺清晰的看到哪些 KVC配置 在哪個 App 版本上存在問題;
使用指令碼監控與KVC相關的錯誤日誌。如果監控到有錯誤日誌上報,則傳送郵件通知給相關人員;

因此,SDK 採用此方案優化之後,KVC配置 的管理工作就只有1個了:

根據Log資訊快速找到對應的 KVC配置,並上傳一個針對新版本的 KVC配置

Q2: 對於 “內容與位置” 可能會隨時間而變動時,如何實現資料收集與統計?
A2: 使用圈選SDK與資料SDK共同完成動態資料的收集與統計
這個問題在實際產品中也比較常見,比如 App 首頁的內容大多是通過後臺配置的。
這個問題其實可以轉化或分解成如下的2個情況:

同一位置會顯示不同的內容
同一內容會顯示在不同的位置

注意,這2個並非同一個,它們分別對應於不同的場景,同時資料收集的方案也有所不同。
另外,“位置” 可以是在列表中,也可以是非列表中的,不過這個對整體的方案沒有太大影響,僅僅是在不關心位置時viewPath中的萬用字元位置不同。
A2.1 同一位置顯示不同的內容
例子:在 App 首頁有一個展示最近活動的位置,先展示活動1的圖片,過一段時間運營人員又配成活動2的圖片。如何統計活動1、活動2各自的點選量?
針對這種場景,SDK 的解決方案是:“關心位置” + “關心內容”。
“關心位置” 的意思是隻使用當前的位置,具體表現是viewPath中不包含任何萬用字元;“關心內容” 的意思是指定一個想要統計的內容。
整個過程可以分解為如下3個環節:

圈選SDK上傳“關心位置”的KVC配置。KVC配置中指定獲取活動的url的keyPath。
資料SDK在活動發生點選時,收集當前活動對應的url,並跟隨點選事件一起上報。
圈選SDK上傳“關心位置” + “關心內容”的圈選配置,關心的內容指定為想要統計的活動的url值。

A2.2 同一內容顯示在不同的位置
例子:App 首頁有4個固定的入口,假設其中一個叫“熱門推薦”,那麼根據後臺配置的順序不同,“熱門推薦”可能被顯示在4個位置中的任何1個,即一段時間顯示在第1個,過一段時間可能顯示在第2個位置。這時如何統計出“熱門推薦”的點選量?
針對這種場景,SDK 的解決方案是:“不關心位置” + “關心內容”。
“不關心位置” 是指viewPath中含有萬用字元,用於表示viewTree中的多個位置。例如想要匹配列表所有行時,則將viewPath中的indexPath替換為萬用字元。
這個問題的解決過程也分為如下3步:

圈選SDK上傳“不關心位置”的KVC配置。KVC配置中指定獲取入口的 title 的keyPath。
資料SDK在4箇中任何一個入口被點選時,都去收集入口的 title,並跟隨點選事件一起上報。
圈選SDK上傳“不關心位置” + “關心內容”的圈選配置,關心的內容指定為“熱門推薦”。

到這裡,資料收集與圈選配置的工作都已經做完了,接下來就是後臺的資料統計了。
上述2種情況對後臺進行統計沒有區別,都使用一個統計方案,這裡也介紹一下後臺大概的統計思路:

拿到第3步中上傳的圈選配置,根據viewPath 與 “關心的內容” 生成一個正則表示式,然後從資料 SDK 上報的原始資料中進行正則匹配,進而統計出相應資料。