Android無埋點數據收集SDK關鍵技術

前言

  鑑於日益強烈的精細化運營需求,網易樂得從去年開始構建大數據平臺,<<無埋點數據收集SDK>>所以立項,用於向大數據平臺提供全量,完整,準確的客戶端數據.
  <<無埋點數據收集SDK>>Android端從着手,到經歷重構,逐步完善到如今已經有快一年的時間了.期間從開源社區以及同行中獲得了一些頗有意義的技術參考,所以在這個SDK趨於完善的今天,咱們也考慮將這一路在技術上的探索經歷和收穫分享出來.javascript

  1. 4月16-18日,QCon北京2017全球軟件開發大會上有同事表明Android/IOS兩端進行統一的技術分享,歡迎你們前去交流
  2. 咱們會逐漸整理一些技術文章出來

  以前關於Android端的<<無埋點數據收集SDK>>使用的技術,寫了一篇文章Android AOP之字節碼插樁,這個是Android端進行一切收集的起點,咱們就是用這個方法輕鬆拿到各類"Hook"點的.
  本篇文章則接着講一下關於收集SDK內部收集邏輯的一些關鍵技術.java


目錄

1、概述
1.1 SDK數據收集能力現狀
1.2 關鍵技術點概述
2、View的惟一標識(ID)
2.1 調研
2.2 利用ViewTree構建ViewID
2.3 ViewPath的生成
2.4 ViewPath的優化
3、頁面的劃分
3.1 合理劃分頁面的重要性
3.2 Android中的頁面
3.3 頁面名組成
4、無需埋點輕鬆收集定製的業務數據
4.1 配置示例
4.2 無埋點收集流程
4.3 數據路徑(DataPath)
5、結語android


1、概述

  本部分首先簡要介紹一下咱們的收集方案目前能夠收集到哪些數據,而後對於本文重點介紹的三個技術點進行概述.程序員

1.1 SDK數據收集能力現狀

  目前咱們的SDK進行數據收集時基本有兩個能力:數組

a. 通用數據全量收集
  通用數據指的是與業務無關的用戶行爲數據,不管是電商應用仍是社區應用,接入SDK後通用數據的收集上都是無差的,這些通用數據大體有:緩存

事件 描述
冷啓動事件 App第一次啓動時的,版本號、設備ID、渠道、內存使用狀況,磁盤使用狀況等信息
先後臺事件 App進入前臺或者後臺
頁面事件 頁面(Activity或Fragment)顯示(Show)/隱藏(Hide)
控件點擊事件 某個控件(包括頁面上控件和彈窗中控件)被用戶點擊
列表瀏覽事件[可選] 某個列表的哪些條目被用戶瀏覽了
位置事件[可選] 上報用戶地理位置信息
其它事件 省略描述

b. 業務相關數據需求經過下發配置進行無埋點定製收集
  除了上述通用數據,與具體業務相關的數據收集。拿網易貴金屬的首頁舉個例子:架構

圖1-1 無埋點收集業務數據示例

  假使須要在用戶點擊上圖紅框區域時,把「粵貴銀」這個交易品的ID(或者下方顯示的指數等,只要在內存中存在的數據均可以)一塊兒報上來。
  對於此種需求,數據收集SDK作到了無需埋點不依賴開發週期,經過線上下發一些配置信息,便可即時進行數據收集。具體原理第四節敘述。app

1.2關鍵技術點概述

a. View的惟一標識(ID),(詳見本文第二節)
  當咱們收集控件數據時碰到的第一個問題就是:如何把界面上的任何一個View與其餘View區分開來.iphone

好比:某個Button被點擊了
咱們在上報數據的時候須要把這個Button和其餘全部控件(好比另外一個Button,另外一個ImageView等)區分開來,這樣這條上報的數據才能表示"就是那個Button被點擊了一下".ide

  這就須要爲界面上的每個控件生成一個惟一的ID. 此ID除了具備區分性,還須要用於一致性一致性是同一個View不管界面佈局如何動態變化,或者說屢次進入同一頁面,此ID須要保持不變.

b. 頁面的劃分,(詳見本文第三節)
  除了Activity有些Fragment也須要看做頁面,這就要求:

  • 在Fragment show/hide時上報相關頁面事件.
  • 頁面Fragment中發生的用戶交互事件也須要歸於此Fragment頁面,即點擊某個View須要上報頁面Fragment的信息(從View中怎麼獲取Fragment信息?)

c. 無需埋點輕鬆收集定製的業務數據,(詳見本文第四節)
  如前面所述,默認狀況下數據收集SDK會收集全量的用戶交互數據,對於定製的業務收集需求,數據收集SDK也作到了無需代碼埋點,經過線上下發一些配置進行即時收集


2、View的惟一標識(ID)

2.1 調研

  用於區分界面上每一個View的ID? Android系統是否提供給了咱們這個ID?

確實,Android系統提供了一個ID,view.getId()便可得到一個int型的id用於區分View,可是這個ID由於如下兩個緣由卻並不能知足咱們的須要.

  1. 有至關一部分view是NO_ID,好比在佈局文件中未指定id,或者直接在代碼裏面new出來view,view.getId()返回的所有都是NO_ID
  2. 這個ID是不穩定的,因爲這個ID其實就是每次編譯產生的R文件中的int常量,所以同一個按鈕,兩個版本編譯出來的ID極可能時不同的.

所以,咱們只能本身動手構建咱們的ID嘍,怎麼構建?答案是利用所屬Page+ViewTree構建ViewID.

2.2 利用ViewTree構建ViewID

  在Android的概念裏,每一個Window(ActivityWindow/DialogWindow/PopupWindow等)上面都生長着一棵ViewTree.而屏幕中看到的各類控件(ImageView/Button等)都是這棵ViewTree上的節點.
  有Android開發環境的同窗只須要打開AndroidDeviceMonitor-dump view hierarchy 就能夠看到ViewTree的模樣,以下圖:

圖2-1 ViewTree概念圖

所以,咱們萌生出一個想法:

利用Page+ViewTree中的位置構建ViewID.

View在ViewTree中的位置主要用兩點來肯定:

  • 縱向的深度
  • 橫向的index

考慮這兩個因素後,咱們定義一個ViewPath:

ViewPath:當前view到ViewTree根節點的一條路徑,用於在ViewTree中惟必定位當前view。路徑中的每一個節點包含兩部分信息,即節點View類型信息,以及節點View在兄弟中的index。

以下圖,是一個簡單的ViewTree模型(簡單到深度只有兩層,每層只有兩三個控件)

圖2-2 ViewTree模型圖

按照以前給的定義,上圖中控件1,2,3,4的ViewPath以下

控件1ViewPath: RootView/LinearLayout[0]   index爲1表示此節點是兄弟節點中第一個控件
控件4ViewPath: RootView/LinearLayout[0]/ChildView1[0]
控件2ViewPath: RootView/RelativeLayout[1]
控件3ViewPath: RootView/LinearLayout[2]複製代碼

上述給出的ViewPath中,每一個節點(除了首節點)有兩部份內容:

  • LinearLayout,RelativeLayout,ChildView1等ViewType信息(節點View的類型
  • "[]"內的index信息,此index指示此節點是兄弟節點的第幾個

這是最初的ViewPath,用ViewPath定位view,有兩點特別重要:

  • 一致性: 同一個view的ViewPath在ViewTree的動態變化中應保持不變
  • 區分度: 不一樣view的ViewPath應該不一樣

按照這個最初的ViewPath定義在實踐中還不能在一致性和區分度上知足咱們的需求,後面會對ViewPath進行優化。

2.3 ViewPath的生成

  上面咱們由構建ViewID的需求引出了ViewPath的定義,那麼當交互事件(例如:按鈕點擊)發生時,咱們如何生成此控件的ViewPath?
  如上一篇文章< > 所述,當用戶點擊某個按鈕時,咱們插入OnClickListener.OnClick方法中的以下代碼將會被調用:

Monitor.onViewClick(view);複製代碼

上面,入參view即爲當前被點擊的view,獲取此view的ViewPath僞代碼以下:

public static ViewPath getPath(View view) {
    do {
      //1. 構造ViewPath中於view對應的節點:ViewType[index]
      ViewType=view.getClass().getSimpleName();
      index=view在兄弟節點中的index;
      ViewPath節點=ViewType[index];
    }while ((view=view.getParent())instanceof View);//2. 將view指向上一級的節點
  }複製代碼

構造出來的ViewPath以下面例子所示:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]複製代碼

2.4 ViewPath的優化

a. 一致性優化1
情景:

在圖2-2 ViewTree模型圖中,若是像下面圖中所示,在控件2和3中動態插入一個FrameLayout呢?

圖2-3 Android界面動態性變化情景1

此時按照原始ViewPath的定義,咱們來看看控件3的ViewPath發生了哪些變化?

ViewTree動態變化前: RootView/LinearLayout[2]
ViewTree動態變化後: RootView/LinearLayout[3]複製代碼

優化:

ViewPath節點中index的含義從「兄弟節點的第幾個」優化爲:「相同類型兄弟節點的第幾個」

優化後,發生圖2-3所示界面佈局動態變化時,控件3的ViewPath變化爲:

ViewTree動態變化前: RootView/LinearLayout[1]   index爲1表示此節點是兄弟節點中第二個LinearLayout
ViewTree動態變化後: RootView/LinearLayout[1]複製代碼

能夠看出,此處優化使控件3的ViewPath在ViewTree動態插入除了LinearLayout以外其它任何類型時都保持先後一致。

b. 一致性優化2
情景:

在圖2-2 ViewTree模型圖中,若是像下面圖中所示,在控件2和3中動態插入一個LinearLayout時,控件3的ViewPath可否繼續保持先後一致?

按照上述情景,控件3ViewPath的變化以下:

ViewTree動態變化前: RootView/LinearLayout[1]   index爲1表示此節點是兄弟節點中第二個LinearLayout
ViewTree動態變化後: RootView/LinearLayout[2]   前面插入一個LinearLayout致使此節點變爲兄弟節點中第三個LinearLayout了複製代碼

問題
上述情景指的實際上是一個問題:ViewTree中同類型兄弟節點動態變化(插入/移除/移位)影響ViewPath的一致性

  • ViewPath節點中的index,在同類型(ViewType相同,例如都是LinearLayout)兄弟節點動態加入/刪除時,當前節點的index沒法在變化先後保持一致。
  • 「一致性優化1」中的優化能夠抵禦不一樣類型兄弟節點的影響,卻對同類型兄弟節點的影響迫不得已

從ViewPath的定義上難以找到在同類型兄弟節點動態變化先後保持一致的方法,但咱們能夠分析發生此種界面動態變化的情景:

  1. 使用Fragment的動態佈局
      Android界面的動態佈局發生情景中,使用Fragment實現界面動態變化的頻率和影響控件數量仍是比較大的(相對於直接addView())
  2. ListView(等可複用View)中同類型的itemViews。
      此種狀況雖然沒有發生在一個itemView前動態插入一個itemView,可是因爲itemView的複用,致使itemView展現的內容和在父節點listView內的index的對應關係動態變化,所以也歸於此類。

2中所說「ListView等可複用View」形成的問題後面會有優化,此處針對1中的情景討論。1中情景發生時以下圖:

圖2-4 使用Fragment形成界面動態性的情景

  上圖中FragmentA,FragmentB,FragmentC的頂層視圖控件所有是LinearLayout同類型),此時這三個Fragment加入的順序將形成ViewPath在此處各類不一致,從而致使ViewPath在動態變化先後不能保持一致(如前面:ViewTree動態變化先後控件3ViewPath的變化所示)。
優化:

在ViewPath節點中,使用Fragment的名字替換ViewType

  優化後,發生圖2-4所示界面佈局動態變化時,控件3的ViewPath變化爲:

ViewTree動態變化前: RootView/FragmentB[0]   index爲0表示此節點是兄弟節點中第一個FragmentB
ViewTree動態變化後: RootView/FragmentB[0]複製代碼

  如上,這次優化使得,在頂層視圖ViewType相同的Fragment動態添加/刪除到ViewTree時,ViewPath在變化先後保持一致。

c. 針對可複用View的優化
情景
  以最常使用的ListView爲例,假設有一ListView滿屏只顯示3個條目,那麼此ListView可能只有3個子控件(ItemView),而此ListView上滑以後能夠顯示100項內容
  這3個ItemView與100項內容是一對多的對應關係,並且映射並沒有可靠規律。
  此時,咱們但願ViewPath能夠區分這100項顯示的內容條目,而非僅僅區分3個ItemView

上面情景中的問題可用下圖表達:

圖2-5 可複用View的ViewPath區分性優化

  如上圖中,內容條目1和4都是用itemView1來呈現的,按照以前的ViewPath定義,圖2-5中各個內容條目的ViewPath以下:

內容條目1: ListView/ItemView[0]   index爲0表示此節點是兄弟節點中第一個ItemView
內容條目4: ListView/ItemView[0]   
內容條目2: ListView/ItemView[1]  
內容條目3: ListView/ItemView[2]複製代碼

  能夠看出內容條目1和4的ViewPath區分不開。此種問題能夠總結爲:

顯示內容與ViewTree中的控件不是一一對應的狀況形成基於ViewTree的ViewPath區分度不夠

  • 可複用View,好比:ListView,RecyclerView,Spinner等,呈現出來子View的數目和實際子View的數目未必一致
  • ViewPager設置緩存頁面數爲1,第二頁顯示時,第二個頁面頂級View實際上是ViewPager的第一個ChildView。此種狀況也會形成顯示內容(第二頁)與ViewTree中的控件(第一個ChildView)不對應的狀況。

所以咱們對於ViewPath做以下優化:

ViewPath節點的index取內容的第幾項,而非第幾個ItemView。

優化:
優化後圖2-5中各個內容條目的ViewPath以下:

內容條目1: ListView/ItemView[0]   index爲0表示此節點是ListView顯示的第一個內容條目
內容條目4: ListView/ItemView[3]   
內容條目2: ListView/ItemView[1]  
內容條目3: ListView/ItemView[2]複製代碼

可見,以前ViewPath沒法區分的內容條目1和4如今能夠區分開了。各類可複用View取內容的第幾項的代碼方法以下:

ListView,Spinner等AdapterView------------ListView.getPositionForView(itemView)
RecyclerView------------------------------------RecyclerView.getChildAdapterPosition(itemView)
ViewPager----------------------------------------ViewPager.getCurrentItem()複製代碼

d. ViewPath起點優化
  ViewPath從ContentView爲起點,而非DecorView

  • DecorView : Window上的根視圖,ViewTree中的根,最頂層視圖
  • ContentView: 客戶端程序員定義的全部視圖的父節點,如Actvity中常見的setContentView(view)

一個實際中的ViewPath以下:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]複製代碼

  上面的「ContentFrameLayout[0]」這個節點表明的就是ContentView,程序員在xml或者代碼裏面構建的View都在ContentView中。

  從DecorView到「ContentFrameLayout[0]」的這一段Path是Android系統Framework層決定的,理論上應該是一致的,可是因爲碎片化等緣由可能ViewPath的這一段發生變化.在實踐中,咱們也發現確實有一些Rom發生了此類狀況,可是比率很小.
  爲了屏蔽這種可能形成同一個View在不一樣設備上產生ViewPath不一樣的狀況,ViewPath的起點定義在ContentView比較好.如上面的ViewPath可優化爲:

ContentView/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]#mybutton複製代碼

作法:
  構造每個ViewPath節點時能夠取view.getId(),看看id的packageId部分是否是系統的(系統資源id以16進制的0x01,0x00開頭),若是是,生成ViewPath時屏蔽這段便可.


3、頁面的劃分

3.1 合理劃分頁面的重要性

  頁面在Android中對應於Activity和部分Fragment(好比不少app首頁多tab的設計,若每一個tab是使用Fragment實現的,那麼這種tab通常看做一個頁面).頁面的劃分很重要,由於兩點:

  1. 對於頁面,須要獲取Show/Hide兩個時機,在此時機上報頁面Show/Hide事件,非頁面則不須要
  2. 頁面的劃分關係着用戶交互事件的所屬,例如,按鈕點擊事件上報格式以下:
事件名稱 所屬頁面 ViewPath 其餘屬性
ButtonClicked MainActivity XXX 省略

  表格中的"所屬頁面"表示這次按鈕點擊事件發生在MainActivity中.將交互事件歸屬於頁面這樣對後面不管是進行路徑分析仍是統計控件點擊量分佈都有很大的好處.

3.2 Android中的頁面

  Android中一般須要看做頁面的有Activity和Fragment(對於像全屏Dialog或者全屏的View暫不考慮).對於Activity,上節中提到的兩點都很容易辦到.

a. Activity頁面

  1. 從Application.ActivityLifecycleCallbacks的onActivityResumed/onActivityPaused這兩個回調方法就能夠分別獲得Activity頁面Show/Hide的時機,並在此時機上報相應頁面事件
  2. 交互歸屬的Activity頁面能夠經過Context輕鬆得到,例如上篇文章< > 提到,當按鈕點擊時,會觸發咱們插樁的代碼:
    Monitor.onViewClick(view)複製代碼
    入參view即爲咱們點擊的view,經過view.getContext()咱們通常就能夠獲得此View所屬的Activity,僞代碼以下:
    //從View中利用context獲取所屬Activity的名字
    public static String getActivityName(View view) {
     Context context = view.getContext();
     if (context instanceof Activity) {
       //context自己是Activity的實例
       return context.getClass().getSimpleName().;
     } else if (context instanceof ContextWrapper) {
       //Activity有可能被系統"裝飾",看看context.base是否是Activity
       Activity activity = getActivityFromContextWrapper((ContextWrapper) context);
       if (activity != null) {
         return activity.getClass().getSimpleName();
       } else {
         //若是從view.getContext()拿不到Activity的信息(好比view的context是Application),則返回當前棧頂Activity的名字
         return currentActivityName;
       }
     }
     return "";
    }複製代碼

b. fragment頁面
  相對於Activity,將某些Fragment看做頁面的邏輯就要稍微複雜一些了.這裏面涉及下面幾個問題:

  • 哪些Fragment能夠須要看做頁面?
      這是須要人工決策的,機器作不了這個決定.
      目前咱們這我的工干預是交給用戶研究團隊,全部Fragment截圖等信息均展現在平臺上,由用研同事選擇須要看做頁面的那些,用研選擇的結果將自動化配置到SDK中
  • 如何獲得Fragment頁面的Show/Hide頁面事件?
      因爲fragment使用場景比較多樣,單單依靠OnResume/OnPause兩個回調錶示fragment Show/Hide是不許確的,好比:
    場景一
      首頁一個Activity承載多個Fragment Tab的狀況,此時tab間切換並不會觸發Fragment的OnResume/OnPause.觸發的回調函數是onHiddenChanged(boolean hidden)
    場景二:
      一個ViewPager承載多個頁面的Fragment時
        a.當第一個Fragment1顯示時,雖然第二個Fragment2此時還沒有顯示,可是Fragment2的OnResume卻以及執行,處於resumed的狀態.
        b.ViewPager頁面切換OnResume/OnPause/onHiddenChanged均未觸發,觸發的回調是setUserVisibleHint
      此時判斷Fragment Show/Hide應該用setUserVisibleHint,而非OnResume/OnPause
      如前一篇文章XXX,所述,咱們經過插樁的方式Hook到了fragment的以下生命週期函數用於包裝成爲Show/Hide事件:
    onResume()
    onPause()
    onHiddenChanged(boolean hidden)
    setUserVisibleHint(boolean isVisibleToUser)複製代碼
    使用這幾個回調包裝成適用於各類情景的FragmentShow/Hide事件的僞代碼以下:
    //此回調發生,則證實是場景一中使用情景,
    onHiddenChanged(boolean hidden) {
      hidden == true ------FragmentShow
      hidden == false------FragmentHide
    }
    //場景二中ViewPager頁面切換時觸發Fragment的此回調,
    setUserVisibleHint(boolean isVisibleToUser) {
      if (fragment.isResumed()) {//只有resumed狀態的fragment適用此情景
        isVisibleToUser == true ------FragmentShow
        isVisibleToUser == false------FragmentHide
      }
    }
    //上述使用情景以外的通常場景
    OnResume/OnPause{
     //fragment沒有被hide,而且UserVisibleHint爲可見的情景
      if (!fragment.isHidden() && fragment.getUserVisibleHint()) {
        OnResume ------ FragmentShow
        OnPause  ------ FragmentHide
      }
    }複製代碼
  • 如何將Fragment內部的交互歸屬到Fragment頁面,也就是說如何在交互發生時從view實例拿到Fragment頁面的名字(像以前拿到Activity頁面名字同樣)?
      view能夠經過context拿到Activity的信息,可是卻沒有途徑拿到fragment的引用。那麼,當某個View交互發生,咱們又須要獲取Fragment頁面名字的狀況下,咱們只能事先將Fragment頁面名寫入此View的屬性中。
      作法大體以下:
        a. 按照前一篇文章xxx裏面的方法,在Fragment.OnCreateView方法的結尾插樁,拿到return的view(即爲此Fragment的頂層視圖)
        b. 判斷此Fragment是否被指定爲Fragment頁面,若是是,下一步
        c.遍歷以Fragment的頂層視圖爲根節點的ViewTree, 將Fragment名設置到此ViewTree的每個view上。設置方法以下所示:
    view.setTag(0xff000001, fragmentName);複製代碼
    注意:View類有兩個名爲setTag的方法
    public void setTag(final Object tag)複製代碼
      此方法,類內部用一Object對象存儲tag,protected Object mTag = null;。listAdapter中經常使用於設置holder。咱們此處用的不是這個,不會於此用法衝突
    public void setTag(int key, final Object tag)複製代碼
      此方法,類內部有一稀疏數組存儲tag,private SparseArray mKeyedTags;
      tag的key官方推薦資源id,所以咱們能夠選用相似0xff000001之類的app用不到的資源id進行tag存儲以免衝突
        d. 當須要使用Fragment名時,以下調用便可得到:
    view.getTag(0xff000001)複製代碼

3.3 頁面名組成

前面講了將交互事件(好比點擊事件)歸屬到某一個頁面的方法是:

在交互事件中設置一個字段,值爲頁面名稱。

頁面能夠是Activity或者Activity承載的Fragment,咱們的頁面名稱組成以下:

Activity類名[Activity別名][Fragment類名][Fragment別名]複製代碼

說明以下:

  1. 「[]」內的組成部分是可選的,可能有可能沒有。另外,各個組成部分之間有分隔符分割。
  2. 頁面名組成中,Activity的描述(類名/別名)是第一層,Fragment的描述(類名/別名)是第二層
  3. 別名的出現是爲了解決單純依賴類名沒法精確區分頁面的某些狀況,好比:
    在某個電商應用中,「商品詳情頁」(同一個Activity)用於展現各類商品(iphone,電視等),若是須要把「不一樣商品的商品詳情頁「區分紅不一樣頁面來統計pv等指標的話,須要設置別名,如:
    商品詳情頁#iphone
    商品詳情頁#電視複製代碼
    對於別名的設置,須要程序員在業務代碼裏面(如Activity.OnCreate,Fragment.onCreate等)顯式設置.

4、無需埋點輕鬆收集定製的業務數據

4.1 配置示例

  以前提到過,數據收集SDK能夠經過配置下發即時收集定製的數據,那麼在Android端這個是怎麼作到的呢?
首先,看一下下發的配置樣例:

//第一部分:描述
PageName:MainActivity
ViewPath:DecorView/.../ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]
EventType:ViewClick
//第二部分:數據路徑(當描述符合時,按照此路徑取數據)
DataPath:this.context.demoList[5]複製代碼

上面例子翻譯成數據需求就是:

1. 當頁面(MainActivity)
2. 中的控件(DecorView/.../ViewPager[0]/ButtonFragment[0]/AppCompatButton[0])
3. 發生點擊事件(ViewClick)時
4. 按照路徑(this.context.demoList[5])取出數據
5. 並附加到點擊事件上面一塊兒上報複製代碼

按照這個描述,咱們還能夠描述以下等等各類數據需求:

當(某頁面)發生事件(Show)時,按照路徑(xxx)取出數據,並附加到頁面Show事件上面一塊兒上報複製代碼

總結下描述的組成部分,以下:

第一層 第二層 含義
描述部分 頁面 限定頁面
ViewPath 限定按鈕
EventType 限定時機(點擊/前臺/PageShow)
數據路徑 一種DSL,指示目標數據在內存中的位置(可理解爲「引用路徑」)

4.2 無埋點收集流程

  上節展現了用於無埋點定製業務數據收集的配置,那麼SDK收到這樣的一份配置如何最終把想要的數據收集上來呢?

  • 步驟一:產生原始事件。好比點擊時收集,當點擊時會觸發咱們插樁的代碼,並生成原始的點擊事件
    Monitor.onViewClick(view)複製代碼
  • 步驟二:匹配配置
    在onViewClick方法中匹配下發的配置信息,看看Page,ViewPath是否與當前view匹配,EventType是否與當前事件類型匹配,若匹配則進行下一步
    注:ViewPath的匹配能夠有精確匹配和模糊匹配,精確匹配時一個ViewPath精確匹配惟一一個控件.模糊匹配時一個ViewPath可匹配多個控件,例如能夠用用一個ViewPath模糊匹配一個列表中的全部條目.
  • 步驟三:按照數據路徑(DataPath)逐級反射拿到目標數據,並將找到的數據附在原始的點擊事件上進行上報。

4.3 數據路徑(DataPath)

  上述步驟三進行數據收集主要是按照DataPath的描述進行(例如示例中提到的"this.context.demoList[5]"),DataPath是一種咱們用於收集定製數據而定義的一種DSL.含義以下:

a. 含義

DataPath: 指向要收集的目標數據的一條引用路徑,解析此路徑並逐級反射最終拿到目標數據.

  DataPath寫法中的一些關鍵字(符):

關鍵字(符) 含義
. 表示對象所屬關係,如:a.b 表示實例a中的字段b
.() 表示公有方法調用,如:a.b() 表示調用實例a中的方法b.注意:方法入參能夠是DataPath指向的Object
[] 數組/線性表的index. 注意:此index能夠是常量數字,也能夠是一個DataPath指向的數字
this DataPath字符串的起點,表示起點爲當前實例(當前View)
item DataPath字符串的起點,表示起點爲當前View父節點中AdapterView adapter中當前條目. 經常使用於列表中的數據獲取
parent DataPath節點中的關鍵字,用於表示當前view的parentView.效果同view.getParent(),使用此關鍵字可減小視圖引用中的反射
childAt(x) DataPath節點中的關鍵字,用於表示當前view的第x個childView.效果同view.getChildAt(x),使用此關鍵字可減小視圖引用中的反射

b. 應用示例
  下面用兩個例子說明如何從DataPath找到目標數據.

圖4-1 DataPath示例

示例1:列表數據獲取
  上圖中顯示是一個列表,紅框中是列表的第一個條目.那麼,若是咱們想要在列表中條目點擊時,將列表展現的交易品ID(或者合做方ID)等不在界面上顯示而又存在於內存中的數據跟隨點擊事件上報.此處DataPath該怎麼寫?

item.productId複製代碼

  DataPath解釋:

  1. 起點定爲"item",則表示今後ListView(或者RecylerView)綁定的Adapter中當前數據item爲起點取數據.
    假設此ListView綁定的Adapter以下:
    public class DemoAdapter extends BaseAdapter {
    private ArrayList<DataItem> mDataItems;
    ......
    }複製代碼
    則此處"item"表明的就是mDataItems[x] (x表示當前被點擊條目的itemId)

2."productId"是model類DataItem中表示"交易品ID"的字段名稱.

  經過DataPath獲取數據:

  1. 當第x條目被點擊時,若是發現有匹配的配置,對於起點爲"item"的DataPath,先經過view.getParent找到上層ListView實例,而後經過listView.getAdapter()得到綁定的Adapter實例,最後經過Adapter.getItem(ListView.getPositionForView(itemView))獲得數據中第x個item,即mDataItems[x]
  2. 反射獲取mDataItems[x]中的productId字段,便可獲得第x個條目的"交易品ID",將此ID跟隨第x條目的點擊事件進行上報便可.

實例2:界面數據獲取
  一樣時圖4-1所示,加入咱們想在列表中條目點擊時,將條目中展現的"最新價"跟隨點擊事件上報.此處DataPath該怎麼寫?
  紅框所示ViewTree子樹以下:

圖4-2 列表Item ViewTree子樹結構

  如上圖,選中部分是列表的ItemView(RelativeLayout),可見"最新價"是由index爲2的TextView所展現,由此可得,列表中條目點擊獲取"最新價"數據的DataPath以下:

this.childAt(2).mText複製代碼

  DataPath解釋:

  1. 起點爲"this",表示當前被點擊的view實例(圖4-2中被選中的RelativeLayout)
  2. "childAt(2)"表示RelativeLayout.getChildAt(2),獲得圖4-2中index爲2的TextView
  3. "mText" 表示取出步驟2中獲得TextView實例的mText字段(TextView控件顯示的文字內容存儲在mText字段內)
  4. 將取出的界面上顯示的"最新價"數據添加到原始點擊事件中,一塊兒上報.

c. DataPath注意事項:
1.混淆.
  因爲DataPath本質上描述的時內存中的"引用路徑",而且按照DataPath取數據時用了反射的方法,所以DataPath應該描述的是混淆以後的"引用路徑".
  雖然DataPath可能受到混淆的影響,可是

* 用於存儲數據的model類一般是不被混淆的.如咱們以前的item關鍵字直接將起點設置爲列表條目的model類對象,不受混淆影響.
* 經過關鍵字parent/childAt(x)能夠在視圖的引用中不受混淆影響
* 接口的方法一般不受混淆影響.所以在DataPath中多用接口方法調用複製代碼

  所以開發在配置DataPath時應儘可能用上述不被混淆影響的字段及方法.可是,若是真的用到了混淆過的字段怎麼辦.咱們的方案是:

數據報警

  好比版本1上配置的DataPath "a.b",在升級新版本2後再也不適用,則新版本2按照"a.b"收集時將收集不到,產生報警信息到後臺.後臺收到大量此種信息會提醒開發爲新版本配置適用新版本的DataPath.

2.代碼變化致使引用路徑變化,從而導致以前配置的DataPath失效.
  與代碼中埋點相比,線上配置進行收集數據與代碼的變化是並行的,無關的.這就有可能形成原有代碼修改致使DataPath失效.其實若是客戶端架構設計合理,功能迭代更可能是在進行代碼的擴展,而非修改,這種致使DataPath失效的狀況應該會大大下降的.
  可是不管如何:

配置的DataPath擺脫不了與版本的相關性

  對於此種問題咱們依然是經過前面提到的"數據報警"進行監控及避免的.


5、結語

  綜上,本文介紹了數據收集邏輯中3個比較關鍵的點(ViewID/Page/DataPath),結合上一篇文章的(AOP原理),Android端無埋點數據收集技術上比較關鍵的點皆以總結完畢.  固然實現SDK過程當中遭遇過不少比較有意思的技術問題,後續也會陸續進行整理.

相關文章
相關標籤/搜索