網(wǎng)易HubbleData無埋點SDK在iOS端的設(shè)計與實現(xiàn)

0 引言

最近在負責(zé)公司的HubbleData的埋點SDK的開發(fā)任務(wù),產(chǎn)品的雛形其實在幾年前就已經(jīng)有了,公司內(nèi)部的諸如考拉、易信、LOFTER、美學(xué)、漫畫等多款產(chǎn)品都已接入使用。

下圖給出HubbleData SDK某個應(yīng)用的部分分析的展示頁面:

(1)概覽示意圖

事件

(2)事件分析示意圖

事件

(3)實時分析示意圖

事件

此外HubbleData平臺還具備留存分析、漏斗分析、粘性分析、數(shù)據(jù)看板等多種功能,方便相關(guān)負責(zé)人員對產(chǎn)品用戶行為進行進一步的探索分析。

老版本的SDK的設(shè)計是代碼埋點實現(xiàn)的,雖然對于一些較為成熟的產(chǎn)品,代碼埋點完全能夠達到產(chǎn)品方的需求,但是對于一些新起步或者需頻繁變更的需求的新產(chǎn)品等,考慮到其維護的成本大,代價高等缺點,HubbleData無埋點SDK的設(shè)計就顯得尤為重要了。

本人主要負責(zé)iOS端無埋點以及可視化圈選的工作,文章主要系統(tǒng)講解一下HubbleData無埋點SDK在iOS端的設(shè)計與實現(xiàn)和一些相關(guān)問題的解決,后續(xù)將針對整個埋點的實現(xiàn)流程與可視化圈選等內(nèi)容再作分享。

一、埋點簡介

1.1 三種埋點的實現(xiàn)方式簡介

埋點的方式分為三類:代碼埋點、可視化埋點和無埋點。這里簡單的介紹一下三種埋點方式:

(1) 代碼埋點即是在代碼的關(guān)鍵部位植入所要收集數(shù)據(jù)的N行代碼,需要挖開產(chǎn)品本身,深入了解產(chǎn)品的業(yè)務(wù)邏輯及項目結(jié)構(gòu),下面代碼模擬展示的即是點擊提交訂單的時候HubbleData SDK代碼埋點;

代碼埋點示例

(2) 可視化埋點即用可視化交互的方式圈選出所要采集數(shù)據(jù)的控件,當(dāng)用戶行為產(chǎn)生時,即可收集到相應(yīng)的埋點數(shù)據(jù)。相比于前面的代碼埋點而言,可視化埋點能夠解決代碼埋點代價大成本高的問題,但是無法靈活的自定義埋點屬性。

可視化埋點流程

(3) 無埋點也叫全埋點,即不需要用戶主動埋點,可以收集用戶所有的操作行為,同樣采用可視化圈選,用戶能夠拿到所想采集的埋點數(shù)據(jù),能夠解決可視化圈選中數(shù)據(jù)不可回溯的問題。下圖給出了無埋點數(shù)據(jù)收集的簡單流程。

無埋點數(shù)據(jù)收集流程

HubbleData SDK的設(shè)計主要是代碼埋點結(jié)合無埋點的數(shù)據(jù)采集方式,其中也涉及到可視化埋點中的屏幕序列化及事件綁定機制,本文主要介紹一下無埋點的設(shè)計與實現(xiàn)。

1.2 無埋點SDK設(shè)計詳細流程

下圖給出HubbleData無埋點SDK在iOS端的設(shè)計實現(xiàn):

無埋點詳細設(shè)計流程

從上圖可以看出,HubbleData的無埋點是在代碼埋點的基礎(chǔ)上實現(xiàn)的,所處無埋點的難點也就集中在以下三個方面:

(1)自動獲取埋點的EventID
(2)自動獲取埋點的時機
(3)自動獲取埋點需采集的屬性

本文主要就這三個方面進行分析,第二部分主要講一下事件唯一ID的確定,第三部分主要講一下無埋點的采集的實現(xiàn),主要是各種事件發(fā)生采集的時機以及待采集的屬性的配置。

HubbleData SDK還涉及到許多其他功能,包括屏幕序列可視化、代碼埋點、精準渠道追蹤等,這里不再介紹,后面會陸續(xù)分享相關(guān)的技術(shù)實現(xiàn)。

二、事件唯一ID的確定

為了實現(xiàn)在可視化圈選的時的事件的唯一性,每一個無埋點的事件采集都必須有且僅有一個唯一的標識符來區(qū)分不同的事件。不同于代碼埋點,用戶可以自定義的配置自己所需的EventID,無埋點過程中,需要SDK自己配置每一個采集事件的EventID,通過可視化圈選的操作,篩選出相應(yīng)的EventID所對應(yīng)的數(shù)據(jù)信息。HubbleData采用的是構(gòu)造view唯一標識字符串的方式去唯一的標識這樣的一個事件,主要由view的層級結(jié)構(gòu)path路徑、該view的所在頁面類名以及view所帶的一些自身固定屬性等構(gòu)成,并通過SHA256編碼來獲取唯一的EventID。

下面將整體系統(tǒng)介紹一些事件唯一ID的生成過程。

2.1 控件的層級結(jié)構(gòu)path構(gòu)造

2.1.1 普通view的層級結(jié)構(gòu)path構(gòu)造

層級結(jié)構(gòu)path主要是基于頁面的控件樹構(gòu)造而成,每個view都有superview與subviews的屬性,將每一個view的superview作為樹的父節(jié)點,將其subviews作為子節(jié)點,這樣就能把整個app上的所有view組成一棵龐大的控件樹,其中樹的頂層是UIWindow,然后是每一個view節(jié)點依次向下展開。下圖給出一個簡單的控件樹的結(jié)構(gòu)圖。


空間樹結(jié)構(gòu)

下面會詳細介紹一下HubbleData的唯一標識路徑的構(gòu)造方式。

不同類

同類

像上圖1所示,如果一個view的subviews中都是不同類型的,比如像下圖圖1所示的控件樹那樣,可以唯一標識UILabel和UIButton控件為:

UIView_UILabel
UIView_UIButton

但是真正的頁面是不會像理想中的所有控件都是不同類型的,可以說這種極端情況基本不存在,如果還是按照上述的方法來構(gòu)造路徑的話,兩個UILabel都會被標識成UIView_UILabel,這顯然無法區(qū)分兩個控件。因此僅僅是每個控件節(jié)點的路徑名稱是無法唯一標識這個控件的,這里HubbleData加入了此控件節(jié)點在父視圖中的index。比如上圖2,可以將兩個UILabel標識為:

UIView(0)_UILabel(0)
UIView(0)_UILabel(1)

這里假設(shè)父視圖是index為0的一個節(jié)點,這樣就可以完全的區(qū)分出兩個控件了。

那么剩下的問題就是每個UIView index索引值的確定。

每個UIView都有subviews屬性,每一個子視圖都有一個被addsubView的次序,其實要拿的這個index就是子視圖被add的次序,那么該怎么拿到這個次序呢,在蘋果的官方說明文檔中,歲UIView的subviews屬性,是這么介紹的:

@property(nonatomic, readonly, copy) NSArray *subviews

You can use this property to retrieve the subviews associated with your custom view hierarchies. 
The order of the subviews in the array reflects their visible order on the screen.

即每一個子視圖在這個subviews數(shù)組中的索引就是HubbleData要拿的index。

針對復(fù)雜的視圖形式,如下圖所示,按照上述的層級結(jié)構(gòu)路徑構(gòu)造方法得到的唯一層級路徑為:

UIView(0)_UILabel(0)
UIView(0)_UIButton(1)
UIView(0)_UIButton(2)  
混合

從上述的分析可知,按照上述介紹的方法進行view的唯一層級路徑標識,對大部分的頁面來說已經(jīng)足夠,但是對于一些更為靈活點的頁面,由于一些業(yè)務(wù)需求等原因,開發(fā)人員經(jīng)常會調(diào)用removeFromSuperview, insertSubview:atIndex:, insertSubview: belowSubview:等函數(shù),都會極大的影響整個頁面的subviews的索引值,比如現(xiàn)在我將上圖所示的UILabel移動到兩個UIButton的后邊,那么得到的唯一層級路徑為:

UIView(0)_UIButton(0)
UIView(0)_UIButton(1)
UIView(0)_UILabel(2)  
混合

可以發(fā)現(xiàn),唯一層級路徑已經(jīng)被改變,但是整個頁面卻沒有發(fā)生變化,不僅會產(chǎn)生新的事件(比如UIButton(0),UILabel(2)),連UIButton(1)事件的采集也會出錯,即使是不同的事件,卻得到了不同的eventID,所以需要提高構(gòu)造的層級結(jié)構(gòu)路徑的穩(wěn)健型。

正像剛剛提到的,不同類型的UIView不需要做index的區(qū)分,那么在獲取這個index的時候,不是簡單的從subviews這個數(shù)組中獲取其對應(yīng)的索引值,而是進行一個簡單的同類歸并再取索引值,一個很簡單的處理。

for (UIView *view in subviews) {
    if ([NSStringFromClass([subview class]) isEqualToString:NSStringFromClass(class)]) { //class為待篩選的類
        [array addObject:view];
    }
}

這樣就可以取得array中的index作為其真正的索引值,得到的層級結(jié)構(gòu)路徑為:

UIView(0)_UILabel(0)
UIView(0)_UIButton(0)
UIView(0)_UIButton(1) 

此時無論UIlabel的位置放在何處,都不會改變這個路徑的構(gòu)造形式,大大增加了穩(wěn)健型。其實也能發(fā)現(xiàn),這僅僅只能提高穩(wěn)健型,并不能從根本上解決這個問題,比如若我把兩個UIButton的順序調(diào)換了,或者刪除了第一個,此時依然會得到一些不準確的層級路徑。此問題會后續(xù)解決,會逐步引入誤差容量和相似度這個概念,即只要在誤差范圍內(nèi),則會進行進一步的匹配,具體的解決方案本篇不在介紹。

2.1.2 幾種特殊情況的處理

2.1.1主要講的是一些普通view的層級結(jié)構(gòu)的path構(gòu)造方式,但是有一些特殊情況需要特別的考慮處理:

  • UITableViewCell

由于UITableViewCell具有可復(fù)用的機制,當(dāng)一個頁面中在持續(xù)滾動的時候,cell在不斷的復(fù)用,如果還使用2.1.1中介紹的方法來獲取index索引值話,那么會引起整個頁面無埋點數(shù)據(jù)采集的混亂。

當(dāng)獲取當(dāng)前UITableViewCell的index時,可以使用indexPath參數(shù)進行替換,這個參數(shù)可準確的獲取section和row的值,唯一的對應(yīng)每一個cell。唯一層級路徑的形式可以自定義配置,HubbleData的設(shè)置方式為:類名+(section: row:),下面給出一個示例:

MyTableViewCell(section:0 row:7)
  • UICollectionViewCell

UICollectionViewCell的path生成原理同UITableViewCell,HubbleData的設(shè)置方式為:類名+(section:item:),下面給出一個示例:

MyCollectionViewCell(section:0 item:7)
  • UIControl

其實UIButton也算是一種普通view的一種,大多數(shù)情況下,使用上述的層級結(jié)構(gòu)path以及頁面類名的組合能夠唯一的確定當(dāng)前UIControl的唯一標識符,但是有一種特殊的情況,當(dāng)作為UINavigationItem時會出現(xiàn)特殊情況,下面的所給出的兩個例子。

bar1
bar2

當(dāng)點擊第一個NavigationBar的右側(cè)的按鈕時,得到的層級路徑為:

...UIViewControllerWrapperView(0)_UIView(0)_UILayoutContainerView(0)_UINavigationBar(0)_UIButton(1)

分析可知,左側(cè)的設(shè)置按鈕的索引為0,所以右側(cè)的按鈕索引為1。同時獲取的當(dāng)前頁面為:UINavigationController。

當(dāng)點擊第二個頁面的同一個類型的按鈕時,即同樣標有數(shù)字7的item時,此時得到的層級路徑為:

...UIViewControllerWrapperView(0)_UIView(0)_UILayoutContainerView(0)_UINavigationBar(0)_UIButton(2)

可以發(fā)現(xiàn)此時的按鈕的索引變成了2,已經(jīng)不同于上述第一個NavigationBar的同一個按鈕的層級路徑了,經(jīng)過分析,索引值為1的按鈕是最右側(cè)的表格的那個item,經(jīng)過驗證可以得到其層級路徑:

...UIViewControllerWrapperView(0)_UIView(0)_UILayoutContainerView(0)_UINavigationBar(0)_UIButton(1)

獲取的頁面為:UINavigationController。

其實這種頁面很常見,由于頁面的切換,NavigationBar上的一些按鈕的位置可能順序會打亂,導(dǎo)致同一個功能的NavigationItem已經(jīng)無法確定標識唯一,即使是獲取了當(dāng)前按鈕所在的頁面也無法區(qū)分,因為獲取的都是UINavigationController。從上面的分析可以看出,這種情況甚至?xí)?dǎo)致嚴重錯亂的數(shù)據(jù)采集。

其實仔細分析一下,如果分析得出該UIControl是在UINavigationBar上,則無需設(shè)置其相應(yīng)的index值,即上述的所有navigationItem的層級結(jié)構(gòu)路徑都為:

...UIViewControllerWrapperView(0)_UIView(0)_UILayoutContainerView(0)_UINavigationBar(0)_UIButton

即都不做區(qū)分。

HubbleData采用增加一種新的屬性來區(qū)分各個item,其實很明顯可以看出來,這個item的執(zhí)行的action肯定是不同的,所以取其action屬性來區(qū)分,最終的區(qū)分形式如下:

path(...UIViewControllerWrapperView(0)_UIView(0)_UILayoutContainerView(0)_UINavigationBar(0)_UIButton)&actions(button1Click:)
path(...UIViewControllerWrapperView(0)_UIView(0)_UILayoutContainerView(0)_UINavigationBar(0)_UIButton)&actions(button2Click:)

這樣,HubbleData就可以準確的區(qū)分不同的item了,同時實現(xiàn)同一種功能的item,由于其action相同,所以也會準確的標識其唯一性。

  • UIAlertController

由于不同的UIAlertController在選擇確定、取消等選項時,選取的進行唯一層級路徑判定的view需要進行一定的處理,同時為了保證不同的UIAlertController處于同一位置的選項的埋點EventID不同,這里在構(gòu)造唯一標志字符串的時候還要加入該UIAlertController的message和title信息。3.5小節(jié)中會進行相關(guān)無埋點采集的介紹。

  • viewController的嵌套

一般情況下,普通的view只需按照一般的層次路徑收集index即可,但是當(dāng)存在pageViewController時,如下圖所示分別給出了一個橫向滾動(以公司考拉app為例)和縱向滾動(以公司嚴選app為例)的app的截圖的示例:

其實可以看出,pageViewController會應(yīng)用到各種各樣app中,所以這類app在使用過程中的無埋點問題尤其要考慮。

(1) 各個子頁面的controller不同?

如果pageViewController中的各個子頁面不同,雖然后續(xù)2.2節(jié)HubbleData會加入頁面controller的信息來區(qū)分這些不同的子頁面,但是可能會由于每個子頁面加入的順序不同,導(dǎo)致每次app進來的時候同一個頁面的事件會獲取不同的EventID,舉例來說明一下,如上圖1所示,比如前四個子頁面是ViewController1, ViewController2, ViewController3, ViewController4,這類pageViewController除非設(shè)置四個子頁面同時預(yù)加載出來,那么此時的獲取的層級路徑為:

ViewController1對應(yīng)路徑為:superview(0)_subControllerView(0) 
ViewController2對應(yīng)路徑為:superview(0)_subControllerView(1)
ViewController3對應(yīng)路徑為:superview(0)_subControllerView(2)
ViewController4對應(yīng)路徑為:superview(0)_subControllerView(3)

但是app基本都不會預(yù)加載出所有頁面,對于用戶不感興趣的頁面完全沒必要一次性全部加載處理,只有當(dāng)用戶選擇了該條目時,該對應(yīng)的子頁面才會加載出來,如果現(xiàn)在用戶點擊的順序是ViewController1,ViewController3,ViewController4,ViewController2,由于addChildViewController或者addSubView的順序的改變,那么此時獲取的層級路徑為:

ViewController1對應(yīng)路徑為:superview(0)_subControllerView(0) 
ViewController2對應(yīng)路徑為:superview(0)_subControllerView(3)
ViewController3對應(yīng)路徑為:superview(0)_subControllerView(1)
ViewController4對應(yīng)路徑為:superview(0)_subControllerView(2)

可以發(fā)現(xiàn),index值變了,層級路徑不唯一了,那么無埋點采集的EventID可能會由于用戶選擇頁面順序的不同而不同,造成埋點數(shù)據(jù)的混亂。

HubbleData對于此類頁面的處理是,遇到此類頁面,即不用index標注,所以會統(tǒng)一的標識成:

ViewController1對應(yīng)路徑為:superview(0)_subControllerView 
ViewController2對應(yīng)路徑為:superview(0)_subControllerView
ViewController3對應(yīng)路徑為:superview(0)_subControllerView
ViewController4對應(yīng)路徑為:superview(0)_subControllerView

后續(xù)可以通過不同的頁面的controller的類名獲取其不同的唯一標識字符串。

(2) 各個子頁面的controller相同?

其實做過此類頁面的基本應(yīng)該都熟悉,很多情況下子頁面都是共用的,只不過是填入的model不同而已,那么遇到這種情況,如果是按照問題1的解決思路,即使按照2.2拿到了當(dāng)前頁面的controller,那么還是無法區(qū)分出這些頁面,所以還是需要設(shè)置新的具有辨識度的index。

其實通過pageViewController可以發(fā)現(xiàn),用戶可以通過左右滑動或者上下滑動來切換子頁面,說明所有的子頁面都是嵌入在一個scrollView之中,那么就可以從這個scrollView入手,重新確定index。下面給出HubbleData解決這個問題的方法。

一開始想使用當(dāng)前scrollView的contentOffset整除此pageViewController的頁面寬度和高度所得到的值作為區(qū)分子頁面的index,但是考慮到可能contentOffset的連續(xù)變化以及子頁面橫跨pageViewController整數(shù)倍寬度的邊界時,可能會導(dǎo)致獲取的index不唯一的情況,所以后來使用該子頁面的起始位置整除pageViewController的相應(yīng)地寬度和高度得到相應(yīng)地index。具體的實現(xiàn)如下,其中controller為當(dāng)前的頁面:

 if (view == controller.view || view == controller.view.superview) {
      NSInteger index_x = view.center.x / [view superview].frame.size.width;
      NSInteger index_y = view.center.y / [view superview].frame.size.height;
      NSString *path = [NSString stringWithFormat:@"%@(indexx:%ld indexy:%ld)",  
                        NSStringFromClass([view class]), index_x, index_y];
  } 

所以同樣針對上述(1)所給出的四個ViewController1,優(yōu)化后的到的唯一的標識為:

ViewController1對應(yīng)路徑為:superview(0)_subControllerView(indexx:0 indexy:0)
ViewController2對應(yīng)路徑為:superview(0)_subControllerView(indexx:1 indexy:0)
ViewController3對應(yīng)路徑為:superview(0)_subControllerView(indexx:2 indexy:0)
ViewController4對應(yīng)路徑為:superview(0)_subControllerView(indexx:3 indexy:0)

這樣即使各個子頁面的controller相同,也能通過優(yōu)化后的index來區(qū)分各個不同的子頁面。當(dāng)然這種只是針對嵌套scrollView的子頁面的情況,不過能解決大部分的該類問題,對于一些其他的特殊情況等,需詳細分析頁面布局進行分析。

2.2 當(dāng)前頁面controller的獲取

看上去,大多數(shù)情況下2.1的view的層級結(jié)構(gòu)path已經(jīng)基本確定view的唯一標識字符串,但是普遍存在這么一種情況,當(dāng)同一個頁面跳轉(zhuǎn)兩個不同的頁面時,假如這兩個不同的頁面上都取第一個按鈕的層級路徑,得到的簡化后的結(jié)果都如下所示:

.../UINavigationTransitionView(0)/UIViewControllerWrapperView(0)/UIView(0)/UIButton(0)

是無法進行這兩個頁面上的按鈕區(qū)分的,其實頁面的類名是區(qū)分的一個最直接的方式。HubbleData是按照下面的方法獲取某個view所在的controller的類名的。

獲取當(dāng)前controller示例

將view的層級路徑結(jié)合當(dāng)前頁面的名稱,已經(jīng)能夠解決掉大部分的唯一標識字符串的問題了。

這里需要注意的一點是,當(dāng)頁面類型一樣,只是填充的model不同時,比如瀏覽商品詳情時,所進入的頁面都是一個,只是model不同,目前HubbleData對這種情況暫時未做處理。后續(xù)可參考文章3.2節(jié)UIViewController的無埋點采集,對一些頁面,用戶可以自定義諸如screenTitle的字段,定義該頁面的名稱,比如screenTitle包含產(chǎn)品唯一ID時,此時將該字段加入唯一標識字符串中即可區(qū)分。目前這塊還未做相關(guān)處理,這里只是提供一個簡單的解決思路。

>三、無埋點的采集的實現(xiàn)

3.1 AOP 簡介

下面講一下無埋點的具體實現(xiàn),用到的主要是AOP(Aspect-Oriented-Programming),面向切面編程,面對的是處理過程中的某個步驟和方法。在運行時,動態(tài)的將代碼插入到類的制定方法、指定位置上的編程思想就是面向切面編程。熟悉iOS Runtime的應(yīng)該很清楚,相關(guān)的介紹文章也很多,這里不再過多的贅述。

HubbleData無埋點的實現(xiàn)主要就是借助AOP,hook對應(yīng)類的方法,并在原實現(xiàn)代碼的基礎(chǔ)上插入自己定義的埋點的代碼,當(dāng)該類的被hook的函數(shù)執(zhí)行時,就能實現(xiàn)無埋點數(shù)據(jù)采集的功能。下面給出HubbleData里面Method Swizzling的一個簡單的實現(xiàn)。

Method Swizzling

上述代碼只是給出了一個簡單的實現(xiàn)的邏輯結(jié)構(gòu),new_swizzledMethod也只是selector沒有參數(shù)的情況(除去self和_cmd),真正在埋點的處理過程需要考慮的情況比較多。

3.2 UIViewController的無埋點采集

主要是收集頁面的生命周期,這里HubbleData采用的是hook UIViewController的viewWillAppear方法,按照3.1給出的方式:

 [DASwizzler swizzleBoolSelector:@selector(viewWillAppear:)
                         onClass:[UIViewController class]
                       withBlock:executeAppearBlock];

當(dāng)viewWillAppear函數(shù)執(zhí)行時,插入埋點的代碼。HubbleData的設(shè)計方法為:

EventID設(shè)置為固定的da_screen,即不會通過EventID來區(qū)分各個頁面的信息,HubbleData將各個頁面的區(qū)分信息放在了properties中,其中properties的設(shè)置為:

(1) $screenName 為當(dāng)前頁面的名稱;
(2) $screenTitle 為當(dāng)前頁面的title,可為空;

同時HubbleData SDK提供了一個protocol <DAScreenAutoTracker>

即用戶可以通過實現(xiàn)該protocol,HubbleData SDK會將screenTitle返回的值作為頁面的名稱,trackProperties返回的屬性加入對應(yīng)頁面的da_screen事件的屬性中,作為用戶訪問該頁面時的事件屬性,screenUrl返回的字符串作為頁面的Url,用于做一些頁面之間相互跳轉(zhuǎn)的分析等。

同時增加了白名單設(shè)置,有一些UIViewController的信息用戶不想采集,可以通過設(shè)置白名單的方式,將一些不想采集的UIViewController過濾掉,比如說SFBrowserRemoteViewController,UIInputWindowController等系統(tǒng)自帶的一些。

最后會調(diào)用trackEvent記錄該采集的事件,同上述介紹的代碼埋點一樣,調(diào)用的方法如下:

[[DATracker sharedTracker] trackScreenEvent:@“da_screen” withAttributes:properties];

其中properties即為上述要采集的一些屬性。

3.3 UIControl的無埋點采集

針對UIControl,HubbleData采用的是hook UIControl的sendAction:to:forEvent:方法。由官方文檔可知,在UIControl執(zhí)行對應(yīng)的action時都會首先調(diào)用sendAction:to:forEvent:方法,實現(xiàn)如下:

control

考慮到UIControl的子類較多,所以HubbleData選取了其中使用較多的幾種進行了特殊的分析:主要是UITextField、UIButton和UISwitch,其余的暫時未做特殊分析。具體的埋點的采集設(shè)計為:

無論是哪種UIControl,EventID均采用的是第三部分介紹的唯一標識字符串的SHA256編碼值,但是相關(guān)采集properties有所差別。

3.3.1 UITextField

UITextField是UIControl的一個子類,由于UITextField涉及到用戶的隱私比較多,比如用戶名、密碼、聊天文本等,所以HubbleData不會對此類的UITextField進行埋點的采集。

HubbleData主要采集的是UISearchBar中的UITextField,即UISearchBarTextField,并獲取搜索的文本內(nèi)容,這對于一些電商類的App來說,能夠較好的分析用戶感興趣的商品等,這是作為HubbleData SDK無埋點的一個需求。

hook住sendAction:to:forEvent:后,如果對UISearchBarTextField的所有actions都進行hook的話,那么_searchFieldBeginEditing、_searchFieldEndEditing等所有的action發(fā)生的時候都會進行數(shù)據(jù)的采集,會采集到很多無用的信息,導(dǎo)致采集的數(shù)據(jù)混亂。HubbleData SDK只有當(dāng)_searchFieldEndEditing action發(fā)生時才會進行埋點,收集的properties為:

(1) type 為UIControl采集的事件類型,這里設(shè)置為searchBarEvent;
(2) page 為當(dāng)前頁面的名稱,用于前端顯示用;
(3) searchText 為_searchFieldEndEditing發(fā)生時采集到搜索框的搜索文字(此字段不為空);

這樣就能對搜索框進行無埋點采集,并能收集搜索的文本內(nèi)容。此方法只是在_searchFieldEndEditing發(fā)生時采集數(shù)據(jù),有可能該action執(zhí)行時并未盡興真正的搜索操作,可能會與業(yè)務(wù)數(shù)據(jù)庫的數(shù)據(jù)有出入,但是也能夠較為準確的分析用戶感興趣的搜索內(nèi)容。

3.3.2 UIButton

UIControl中使用最多最常見的是UIButton,因此對UIButton的采集非常重要。在使用UIButton的時候可以隨意的設(shè)置其title等屬性來表示業(yè)務(wù)邏輯的不同狀態(tài)。這里可以舉一個簡單的例子:基本app的登錄頁面,在用戶名和密碼都未輸入時、都輸入時以及登錄中各個狀態(tài),登錄按鈕的title、titleColor等屬性可能都是不同的,即每一種button的樣式都代表著一種樣式,但是得到的EventID是相同的。針對此種情況,HubbleData會加入title、titleColor作為屬性值,以方便后臺進行進一步的分析。

當(dāng)按鈕的兩種狀態(tài)只是兩種不同的背景圖片時,比如微博或者微信的點贊等,其實是變換了一種背景圖片,針對對這種情況處理,HubbleData則會獲取圖片的imageName作為其中一個屬性。

(1) type 為UIControl采集的事件類型,這里設(shè)置為buttonEvent;
(2) page 為當(dāng)前頁面的名稱,用于前端顯示用;
(3) title 為當(dāng)前按鈕的title;
(4) titleColor 為當(dāng)前title的color,會轉(zhuǎn)換成字符串的形式,rgba(r, g, b, alpha);
(5) imageName 為當(dāng)前按鈕的背景圖片的name;
(6) frame 為UIButton的frame,用于分析同類元素,會轉(zhuǎn)換成字符串的形式,rect(x, y, width, height);

可以看出,HubbleData還采集了該view的frame信息,主要是用來分析同類元素用的,下圖給出一個簡單的示例:

button

目前有六個已關(guān)注的產(chǎn)品,當(dāng)想統(tǒng)計用戶所有點贊的事件時,由于每個點贊的按鈕都處于一個UITableViewCell中,在前面介紹的獲取層級唯一路徑UITableViewCell時的特殊處理,由于每個按鈕所在的cell的row不同,所以獲得的每個按鈕的事件的唯一EventID都是不同的,這樣后端在分析的時候,無法歸類同類元素。當(dāng)HubbleData給出frame時,后端可以根據(jù)frame歸類出同一類按鈕的事件,具體的歸類策略這里不再介紹。

3.3.3 UISwitch

類似于UIButton,只不過這里要采集switchState,即當(dāng)前的開關(guān)狀態(tài),具體的采集屬性為:

(1) type 為UIControl采集的事件類型,這里設(shè)置為switchEvent;
(2) page 為當(dāng)前頁面的名稱,用于前端顯示用;
(3) switchState 為switch的開關(guān)狀態(tài);
3.3.4 其余UIControl

其余的只是采集type,page屬性,目前未做過多的處理。

3.4 UITableView和UICollectionView的無埋點采集

針對UITableView和UICollectionView,HubbleData采用的是先hook UITableView和UICoolectionView的setDelegate:方法,然后找到對應(yīng)的delegate,然后再hook delegate類中的tableView:didSelectRowAtIndexPath:方法和UICollectionView的collectionView:didSelectItemAtIndexPath:方法。這里以UITableView為例:

tableview

EventID按照上述介紹的方法獲取,只不過這里要注意的是,獲取的并不是UITableView的唯一標識字符串而是對應(yīng)的點擊的cell的唯一標識字符串。采集的properties為:

(1) type 為UITableView采集的事件類型,這里設(shè)置為tableViewSelectEvent;
(2) page 為當(dāng)前頁面的名稱,用于前端顯示用;
(3) section 為點擊的cell所在的section;
(4) row 為點擊的cell所在的row;

3.5 UIGestureRecognizer的無埋點采集

在iOS開發(fā)中,經(jīng)常會使用一些手勢來處理一些點擊的操作,所以也有必要對UIGestureRecognizer進行hook。HubbleData 并不是直接針對UIGestureRecognizer這個類進行hook,而是hook UIView類的addGestureRecognizer:方法,實現(xiàn)如下:

gesture

通過hook addGestureRecognizer:方法,可以得到該UIView所添加的UIGestureRecognizer,這里只對UITapGestureRecognizer和UILongPressGestureRecognizer進行處理,其他的手勢暫未做處理。得到相應(yīng)的UIGestureRecognizer,添加一個action,當(dāng)該手勢執(zhí)行的時候,同樣會執(zhí)行該action,在action中執(zhí)行埋點的操作。

這里獲取的是UIGestureRecognizer所在的UIView的唯一標識標識字符串編碼作為EventID,采集的屬性為:

(1) type 為UIGestureRecognizer采集的事件類型,這里設(shè)置為gestureTapEvent;
(2) page 為當(dāng)前頁面的名稱,用于前端顯示用;

UIAlertController的特殊處理

這里需要對UIAlertController做一個詳細的說明,因為UIAlertController在點擊諸如取消、確定的選項按鈕時,也會進行手勢的埋點采集,但是在iOS9和iOS10上略微有些區(qū)別。

這里先以iOS9為例,其target是作用在_UIAlertControllerView這個系統(tǒng)的私有類上的,如果直接對這個_UIAlertControllerView進行唯一標識字符串的構(gòu)造,則取消和確定選項得到的EventID是相同的,這樣將無法準確的分析出用戶的選擇,所以必須以每個選項view作為單獨的唯一標識字符串進行分析才能準確區(qū)分。通過獲取_UIAlertControllerView的_actionViews變量,就能得到各個選項的view,這里要做一個簡單的點擊坐標獲取,判斷所點擊的區(qū)域位于的actionView,具體實現(xiàn)如下:

這里在條件判斷時設(shè)定gesture.state == UIGestureRecognizerStateBegan,是由于UILongPressGestureRecognizer會連續(xù)兩次調(diào)用action,因此這里需要加入事件的狀態(tài)進行區(qū)分,防止進行兩次相同的數(shù)據(jù)采集。

iOS10下的UIAlertController的內(nèi)部實現(xiàn)做了一些改動,其target變換成在_UIAlertControllerInterfaceActionGroupView這個系統(tǒng)的私有類上的,然后需要進行一定的處理,獲取UIInterfaceActionSelectionTrackingController的_representationViews變量,遍歷得到各個選項的view,具體實現(xiàn)如下:

通過上述的分析可以發(fā)現(xiàn),這樣雖然能區(qū)分同一個UIAlertController的不同的操作選項,但是可能無法區(qū)分出不同UIAlertController的處于同一位置的選項,所以這里還要加入UIAlertController額外的屬性信息來區(qū)分。

前面也有提過,可以很容易的想到UIAlertController的message和title能夠較好的進行區(qū)分,所以在原有的層級路徑和當(dāng)前頁面的基礎(chǔ)上,還要加上message和title以構(gòu)成唯一標識字符串。給出一個樣例:

path(UIWindow(0)__UIAlertControllerView(0)_UIView(0)_UIView(0)_UIView(0)_UICollectionView(0)__UIAlertControllerCollectionViewCell(section:0 item:0)_UIView(0)__UIAlertControllerActionView(0))&controller(UIAlertController)&message(確認退出群聊嗎?)&title(退群)

四、總結(jié)

文章主要介紹了HubbleData無埋點SDk在iOS端的設(shè)計與實現(xiàn),涉及的主要內(nèi)容:事件唯一ID的確定和部分無埋點的實現(xiàn),當(dāng)然在無埋點SDK的設(shè)計開發(fā)中還遇到了各種各樣的問題。鑒于文章的篇幅已經(jīng)較長,一些問題的解決以及關(guān)鍵技術(shù)的實現(xiàn),比如精準渠道追蹤、hook沖突解決、代碼埋點的實現(xiàn)、屏幕序列化以及可視化圈選部分的內(nèi)容,本篇文章不再介紹,將會在后續(xù)文章中繼續(xù)介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容