Android無(wú)埋點(diǎn)數(shù)據(jù)收集SDK關(guān)鍵技術(shù)

前言

鑒于日益強(qiáng)烈的精細(xì)化運(yùn)營(yíng)需求,網(wǎng)易樂(lè)得從去年開(kāi)始構(gòu)建大數(shù)據(jù)平臺(tái),<<無(wú)埋點(diǎn)數(shù)據(jù)收集SDK>>因此立項(xiàng),用于向大數(shù)據(jù)平臺(tái)提供全量,完整,準(zhǔn)確的客戶(hù)端數(shù)據(jù).
  <<無(wú)埋點(diǎn)數(shù)據(jù)收集SDK>>Android端從著手,到經(jīng)歷重構(gòu),逐步完善到現(xiàn)在已經(jīng)有快一年的時(shí)間了.期間從開(kāi)源社區(qū)以及同行中得到了一些很有意義的技術(shù)參考,因此在這個(gè)SDK趨于完善的今天,我們也考慮將這一路在技術(shù)上的探索經(jīng)歷和收獲分享出來(lái).

  1. 4月16-18日,QCon北京2017全球軟件開(kāi)發(fā)大會(huì)上有同事代表Android/IOS兩端進(jìn)行統(tǒng)一的技術(shù)分享,歡迎大家前去交流
  2. 我們會(huì)逐漸整理一些技術(shù)文章到這個(gè)簡(jiǎn)書(shū)賬號(hào)“移動(dòng)端數(shù)據(jù)收集和分析

之前關(guān)于Android端的<<無(wú)埋點(diǎn)數(shù)據(jù)收集SDK>>使用的技術(shù),寫(xiě)了一篇文章<<Android AOP之字節(jié)碼插樁>>,這個(gè)是Android端進(jìn)行一切收集的起點(diǎn),我們就是用這個(gè)方法輕松拿到各種"Hook"點(diǎn)的.
  本篇文章則接著講一下關(guān)于收集SDK內(nèi)部收集邏輯的一些關(guān)鍵技術(shù).


目錄

一、概述
1.1 SDK數(shù)據(jù)收集能力現(xiàn)狀
1.2 關(guān)鍵技術(shù)點(diǎn)概述
二、View的唯一標(biāo)識(shí)(ID)
2.1 調(diào)研
2.2 利用ViewTree構(gòu)建ViewID
2.3 ViewPath的生成
2.4 ViewPath的優(yōu)化
三、頁(yè)面的劃分
3.1 合理劃分頁(yè)面的重要性
3.2 Android中的頁(yè)面
3.3 頁(yè)面名組成
四、無(wú)需埋點(diǎn)輕松收集定制的業(yè)務(wù)數(shù)據(jù)
4.1 配置示例
4.2 無(wú)埋點(diǎn)收集流程
4.3 數(shù)據(jù)路徑(DataPath)
五、結(jié)語(yǔ)


一、概述

本部分首先簡(jiǎn)要介紹一下我們的收集方案目前可以收集到哪些數(shù)據(jù),然后對(duì)于本文重點(diǎn)介紹的三個(gè)技術(shù)點(diǎn)進(jìn)行概述.

1.1 SDK數(shù)據(jù)收集能力現(xiàn)狀

目前我們的SDK進(jìn)行數(shù)據(jù)收集時(shí)基本有兩個(gè)能力:

a. 通用數(shù)據(jù)全量收集
  通用數(shù)據(jù)指的是與業(yè)務(wù)無(wú)關(guān)的用戶(hù)行為數(shù)據(jù),無(wú)論是電商應(yīng)用還是社區(qū)應(yīng)用,接入SDK后通用數(shù)據(jù)的收集上都是無(wú)差的,這些通用數(shù)據(jù)大致有:

事件 描述
冷啟動(dòng)事件 App第一次啟動(dòng)時(shí)的,版本號(hào)、設(shè)備ID、渠道、內(nèi)存使用情況,磁盤(pán)使用情況等信息
前后臺(tái)事件 App進(jìn)入前臺(tái)或者后臺(tái)
頁(yè)面事件 頁(yè)面(Activity或Fragment)顯示(Show)/隱藏(Hide)
控件點(diǎn)擊事件 某個(gè)控件(包括頁(yè)面上控件和彈窗中控件)被用戶(hù)點(diǎn)擊
列表瀏覽事件[可選] 某個(gè)列表的哪些條目被用戶(hù)瀏覽了
位置事件[可選] 上報(bào)用戶(hù)地理位置信息
其它事件 省略描述

b. 業(yè)務(wù)相關(guān)數(shù)據(jù)需求通過(guò)下發(fā)配置進(jìn)行無(wú)埋點(diǎn)定制收集
  除了上述通用數(shù)據(jù),與具體業(yè)務(wù)相關(guān)的數(shù)據(jù)收集。拿網(wǎng)易貴金屬的首頁(yè)舉個(gè)例子:

圖1-1 無(wú)埋點(diǎn)收集業(yè)務(wù)數(shù)據(jù)示例

假使需要在用戶(hù)點(diǎn)擊上圖紅框區(qū)域時(shí),把“粵貴銀”這個(gè)交易品的ID(或者下方顯示的指數(shù)等,只要在內(nèi)存中存在的數(shù)據(jù)都可以)一起報(bào)上來(lái)。
  對(duì)于此種需求,數(shù)據(jù)收集SDK做到了無(wú)需埋點(diǎn),不依賴(lài)開(kāi)發(fā)周期,通過(guò)線上下發(fā)一些配置信息,即可即時(shí)進(jìn)行數(shù)據(jù)收集。具體原理第四節(jié)敘述。

1.2關(guān)鍵技術(shù)點(diǎn)概述

a. View的唯一標(biāo)識(shí)(ID),(詳見(jiàn)本文第二節(jié))
  當(dāng)我們收集控件數(shù)據(jù)時(shí)碰到的第一個(gè)問(wèn)題就是:如何把界面上的任何一個(gè)View與其他View區(qū)分開(kāi)來(lái).

比如:某個(gè)Button被點(diǎn)擊了
我們?cè)谏蠄?bào)數(shù)據(jù)的時(shí)候需要把這個(gè)Button和其他所有控件(比如另一個(gè)Button,另一個(gè)ImageView等)區(qū)分開(kāi)來(lái),這樣這條上報(bào)的數(shù)據(jù)才能表示"就是那個(gè)Button被點(diǎn)擊了一下".

這就需要為界面上的每一個(gè)控件生成一個(gè)唯一的ID. 此ID除了具有區(qū)分性,還需要用于一致性一致性是同一個(gè)View無(wú)論界面布局如何動(dòng)態(tài)變化,或者說(shuō)多次進(jìn)入同一頁(yè)面,此ID需要保持不變.

b. 頁(yè)面的劃分,(詳見(jiàn)本文第三節(jié))
  除了Activity有些Fragment也需要看作頁(yè)面,這就要求:

  • 在Fragment show/hide時(shí)上報(bào)相關(guān)頁(yè)面事件.
  • 頁(yè)面Fragment中發(fā)生的用戶(hù)交互事件也需要?dú)w于此Fragment頁(yè)面,即點(diǎn)擊某個(gè)View需要上報(bào)頁(yè)面Fragment的信息(從View中怎么獲取Fragment信息?)

c. 無(wú)需埋點(diǎn)輕松收集定制的業(yè)務(wù)數(shù)據(jù),(詳見(jiàn)本文第四節(jié))
  如前面所述,默認(rèn)情況下數(shù)據(jù)收集SDK會(huì)收集全量的用戶(hù)交互數(shù)據(jù),對(duì)于定制的業(yè)務(wù)收集需求,數(shù)據(jù)收集SDK也做到了無(wú)需代碼埋點(diǎn),通過(guò)線上下發(fā)一些配置進(jìn)行即時(shí)收集


二、View的唯一標(biāo)識(shí)(ID)

2.1 調(diào)研

用于區(qū)分界面上每個(gè)View的ID? Android系統(tǒng)是否提供給了我們這個(gè)ID?

確實(shí),Android系統(tǒng)提供了一個(gè)ID,view.getId()即可獲得一個(gè)int型的id用于區(qū)分View,但是這個(gè)ID因?yàn)橐韵聝蓚€(gè)原因卻并不能滿(mǎn)足我們的需要.

  1. 有相當(dāng)一部分view是NO_ID,比如在布局文件中未指定id,或者直接在代碼里面new出來(lái)view,view.getId()返回的全部都是NO_ID
  2. 這個(gè)ID是不穩(wěn)定的,由于這個(gè)ID其實(shí)就是每次編譯產(chǎn)生的R文件中的int常量,因此同一個(gè)按鈕,兩個(gè)版本編譯出來(lái)的ID很可能時(shí)不一樣的.

因此,我們只能自己動(dòng)手構(gòu)建我們的ID嘍,怎么構(gòu)建?答案是利用所屬Page+ViewTree構(gòu)建ViewID.

2.2 利用ViewTree構(gòu)建ViewID

在Android的概念里,每個(gè)Window(ActivityWindow/DialogWindow/PopupWindow等)上面都生長(zhǎng)著一棵ViewTree.而屏幕中看到的各種控件(ImageView/Button等)都是這棵ViewTree上的節(jié)點(diǎn).
  有Android開(kāi)發(fā)環(huán)境的同學(xué)只需要打開(kāi)AndroidDeviceMonitor-dump view hierarchy 就可以看到ViewTree的模樣,如下圖:

圖2-1 ViewTree概念圖

因此,我們萌生出一個(gè)想法:

利用Page+ViewTree中的位置構(gòu)建ViewID.

View在ViewTree中的位置主要用兩點(diǎn)來(lái)確定:

  • 縱向的深度
  • 橫向的index

考慮這兩個(gè)因素后,我們定義一個(gè)ViewPath:

ViewPath:當(dāng)前view到ViewTree根節(jié)點(diǎn)的一條路徑,用于在ViewTree中唯一定位當(dāng)前view。路徑中的每個(gè)節(jié)點(diǎn)包含兩部分信息,即節(jié)點(diǎn)View類(lèi)型信息,以及節(jié)點(diǎn)View在兄弟中的index。

如下圖,是一個(gè)簡(jiǎn)單的ViewTree模型(簡(jiǎn)單到深度只有兩層,每層只有兩三個(gè)控件)

圖2-2 ViewTree模型圖

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

控件1ViewPath: RootView/LinearLayout[0]   index為1表示此節(jié)點(diǎn)是兄弟節(jié)點(diǎn)中第一個(gè)控件
控件4ViewPath: RootView/LinearLayout[0]/ChildView1[0]
控件2ViewPath: RootView/RelativeLayout[1]
控件3ViewPath: RootView/LinearLayout[2]

上述給出的ViewPath中,每個(gè)節(jié)點(diǎn)(除了首節(jié)點(diǎn))有兩部分內(nèi)容:

  • LinearLayout,RelativeLayout,ChildView1等ViewType信息(節(jié)點(diǎn)View的類(lèi)型
  • "[]"內(nèi)的index信息,此index指示此節(jié)點(diǎn)是兄弟節(jié)點(diǎn)的第幾個(gè)

這是最初的ViewPath,用ViewPath定位view,有兩點(diǎn)特別重要:

  • 一致性: 同一個(gè)view的ViewPath在ViewTree的動(dòng)態(tài)變化中應(yīng)保持不變
  • 區(qū)分度: 不同view的ViewPath應(yīng)該不同

按照這個(gè)最初的ViewPath定義在實(shí)踐中還不能在一致性和區(qū)分度上滿(mǎn)足我們的需求,后面會(huì)對(duì)ViewPath進(jìn)行優(yōu)化。

2.3 ViewPath的生成

上面我們由構(gòu)建ViewID的需求引出了ViewPath的定義,那么當(dāng)交互事件(例如:按鈕點(diǎn)擊)發(fā)生時(shí),我們?nèi)绾紊纱丝丶腣iewPath?
  如上一篇文章<<Android AOP之字節(jié)碼插樁>>所述,當(dāng)用戶(hù)點(diǎn)擊某個(gè)按鈕時(shí),我們插入OnClickListener.OnClick方法中的如下代碼將會(huì)被調(diào)用:

Monitor.onViewClick(view);    

上面,入?yún)iew即為當(dāng)前被點(diǎn)擊的view,獲取此view的ViewPath偽代碼如下:

  public static ViewPath getPath(View view) {
    do {
      //1. 構(gòu)造ViewPath中于view對(duì)應(yīng)的節(jié)點(diǎn):ViewType[index]
      ViewType=view.getClass().getSimpleName();
      index=view在兄弟節(jié)點(diǎn)中的index;
      ViewPath節(jié)點(diǎn)=ViewType[index];
    }while ((view=view.getParent())instanceof View);//2. 將view指向上一級(jí)的節(jié)點(diǎn)
  }

構(gòu)造出來(lái)的ViewPath如下面例子所示:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]

2.4 ViewPath的優(yōu)化

a. 一致性?xún)?yōu)化1
情景:

在圖2-2 ViewTree模型圖中,如果像下面圖中所示,在控件2和3中動(dòng)態(tài)插入一個(gè)FrameLayout呢?

圖2-3 Android界面動(dòng)態(tài)性變化情景1

此時(shí)按照原始ViewPath的定義,我們來(lái)看看控件3的ViewPath發(fā)生了哪些變化?

ViewTree動(dòng)態(tài)變化前: RootView/LinearLayout[2]
ViewTree動(dòng)態(tài)變化后: RootView/LinearLayout[3]

優(yōu)化:

ViewPath節(jié)點(diǎn)中index的含義從“兄弟節(jié)點(diǎn)的第幾個(gè)”優(yōu)化為:“相同類(lèi)型兄弟節(jié)點(diǎn)的第幾個(gè)”

優(yōu)化后,發(fā)生圖2-3所示界面布局動(dòng)態(tài)變化時(shí),控件3的ViewPath變化為:

ViewTree動(dòng)態(tài)變化前: RootView/LinearLayout[1]   index為1表示此節(jié)點(diǎn)是兄弟節(jié)點(diǎn)中第二個(gè)LinearLayout
ViewTree動(dòng)態(tài)變化后: RootView/LinearLayout[1]

可以看出,此處優(yōu)化使控件3的ViewPath在ViewTree動(dòng)態(tài)插入除了LinearLayout之外其它任何類(lèi)型時(shí)都保持前后一致。

b. 一致性?xún)?yōu)化2
情景:

在圖2-2 ViewTree模型圖中,如果像下面圖中所示,在控件2和3中動(dòng)態(tài)插入一個(gè)LinearLayout時(shí),控件3的ViewPath能否繼續(xù)保持前后一致?

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

ViewTree動(dòng)態(tài)變化前: RootView/LinearLayout[1]   index為1表示此節(jié)點(diǎn)是兄弟節(jié)點(diǎn)中第二個(gè)LinearLayout
ViewTree動(dòng)態(tài)變化后: RootView/LinearLayout[2]   前面插入一個(gè)LinearLayout導(dǎo)致此節(jié)點(diǎn)變?yōu)樾值芄?jié)點(diǎn)中第三個(gè)LinearLayout了

問(wèn)題
上述情景指的其實(shí)是一個(gè)問(wèn)題:ViewTree中同類(lèi)型兄弟節(jié)點(diǎn)動(dòng)態(tài)變化(插入/移除/移位)影響ViewPath的一致性

  • ViewPath節(jié)點(diǎn)中的index,在同類(lèi)型(ViewType相同,例如都是LinearLayout)兄弟節(jié)點(diǎn)動(dòng)態(tài)加入/刪除時(shí),當(dāng)前節(jié)點(diǎn)的index無(wú)法在變化前后保持一致。
  • “一致性?xún)?yōu)化1”中的優(yōu)化可以抵御不同類(lèi)型兄弟節(jié)點(diǎn)的影響,卻對(duì)同類(lèi)型兄弟節(jié)點(diǎn)的影響無(wú)可奈何

從ViewPath的定義上難以找到在同類(lèi)型兄弟節(jié)點(diǎn)動(dòng)態(tài)變化前后保持一致的方法,但我們可以分析發(fā)生此種界面動(dòng)態(tài)變化的情景:

  1. 使用Fragment的動(dòng)態(tài)布局
      Android界面的動(dòng)態(tài)布局發(fā)生情景中,使用Fragment實(shí)現(xiàn)界面動(dòng)態(tài)變化的頻率和影響控件數(shù)量還是比較大的(相對(duì)于直接addView())
  2. ListView(等可復(fù)用View)中同類(lèi)型的itemViews。
      此種情況雖然沒(méi)有發(fā)生在一個(gè)itemView前動(dòng)態(tài)插入一個(gè)itemView,但是由于itemView的復(fù)用,導(dǎo)致itemView展示的內(nèi)容和在父節(jié)點(diǎn)listView內(nèi)的index的對(duì)應(yīng)關(guān)系動(dòng)態(tài)變化,因此也歸于此類(lèi)。

2中所說(shuō)“ListView等可復(fù)用View”造成的問(wèn)題后面會(huì)有優(yōu)化,此處針對(duì)1中的情景討論。1中情景發(fā)生時(shí)如下圖:

圖2-4 使用Fragment造成界面動(dòng)態(tài)性的情景

上圖中FragmentA,FragmentB,FragmentC的頂層視圖控件全部是LinearLayout同類(lèi)型),此時(shí)這三個(gè)Fragment加入的順序將造成ViewPath在此處各種不一致,從而導(dǎo)致ViewPath在動(dòng)態(tài)變化前后不能保持一致(如前面:ViewTree動(dòng)態(tài)變化前后控件3ViewPath的變化所示)。
優(yōu)化:

在ViewPath節(jié)點(diǎn)中,使用Fragment的名字替換ViewType

優(yōu)化后,發(fā)生圖2-4所示界面布局動(dòng)態(tài)變化時(shí),控件3的ViewPath變化為:

ViewTree動(dòng)態(tài)變化前: RootView/FragmentB[0]   index為0表示此節(jié)點(diǎn)是兄弟節(jié)點(diǎn)中第一個(gè)FragmentB
ViewTree動(dòng)態(tài)變化后: RootView/FragmentB[0]  

如上,此次優(yōu)化使得,在頂層視圖ViewType相同的Fragment動(dòng)態(tài)添加/刪除到ViewTree時(shí),ViewPath在變化前后保持一致。

c. 針對(duì)可復(fù)用View的優(yōu)化
情景
  以最常使用的ListView為例,假設(shè)有一ListView滿(mǎn)屏只顯示3個(gè)條目,那么此ListView可能只有3個(gè)子控件(ItemView),而此ListView上滑之后可以顯示100項(xiàng)內(nèi)容
  這3個(gè)ItemView與100項(xiàng)內(nèi)容是一對(duì)多的對(duì)應(yīng)關(guān)系,而且映射并無(wú)可靠規(guī)律。
  此時(shí),我們希望ViewPath可以區(qū)分這100項(xiàng)顯示的內(nèi)容條目,而非僅僅區(qū)分3個(gè)ItemView

上面情景中的問(wèn)題可用下圖表達(dá):

圖2-5 可復(fù)用View的ViewPath區(qū)分性?xún)?yōu)化

如上圖中,內(nèi)容條目1和4都是用itemView1來(lái)呈現(xiàn)的,按照之前的ViewPath定義,圖2-5中各個(gè)內(nèi)容條目的ViewPath如下:

內(nèi)容條目1: ListView/ItemView[0]   index為0表示此節(jié)點(diǎn)是兄弟節(jié)點(diǎn)中第一個(gè)ItemView
內(nèi)容條目4: ListView/ItemView[0]   
內(nèi)容條目2: ListView/ItemView[1]  
內(nèi)容條目3: ListView/ItemView[2]  

可以看出內(nèi)容條目1和4的ViewPath區(qū)分不開(kāi)。此種問(wèn)題可以總結(jié)為:

顯示內(nèi)容與ViewTree中的控件不是一一對(duì)應(yīng)的情況造成基于ViewTree的ViewPath區(qū)分度不夠

  • 可復(fù)用View,比如:ListView,RecyclerView,Spinner等,呈現(xiàn)出來(lái)子View的數(shù)目和實(shí)際子View的數(shù)目未必一致
  • ViewPager設(shè)置緩存頁(yè)面數(shù)為1,第二頁(yè)顯示時(shí),第二個(gè)頁(yè)面頂級(jí)View其實(shí)是ViewPager的第一個(gè)ChildView。此種情況也會(huì)造成顯示內(nèi)容(第二頁(yè))與ViewTree中的控件(第一個(gè)ChildView)不對(duì)應(yīng)的情況。

因此我們對(duì)于ViewPath作如下優(yōu)化:

ViewPath節(jié)點(diǎn)的index取內(nèi)容的第幾項(xiàng),而非第幾個(gè)ItemView。

優(yōu)化:
優(yōu)化后圖2-5中各個(gè)內(nèi)容條目的ViewPath如下:

內(nèi)容條目1: ListView/ItemView[0]   index為0表示此節(jié)點(diǎn)是ListView顯示的第一個(gè)內(nèi)容條目
內(nèi)容條目4: ListView/ItemView[3]   
內(nèi)容條目2: ListView/ItemView[1]  
內(nèi)容條目3: ListView/ItemView[2]  

可見(jiàn),之前ViewPath無(wú)法區(qū)分的內(nèi)容條目1和4現(xiàn)在可以區(qū)分開(kāi)了。各種可復(fù)用View取內(nèi)容的第幾項(xiàng)的代碼方法如下:

ListView,Spinner等AdapterView------------ListView.getPositionForView(itemView)
RecyclerView------------------------------------RecyclerView.getChildAdapterPosition(itemView)
ViewPager----------------------------------------ViewPager.getCurrentItem()

d. ViewPath起點(diǎn)優(yōu)化
  ViewPath從ContentView為起點(diǎn),而非DecorView

  • DecorView : Window上的根視圖,ViewTree中的根,最頂層視圖
  • ContentView: 客戶(hù)端程序員定義的所有視圖的父節(jié)點(diǎn),如Actvity中常見(jiàn)的setContentView(view)

一個(gè)實(shí)際中的ViewPath如下:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]

上面的“ContentFrameLayout[0]”這個(gè)節(jié)點(diǎn)代表的就是ContentView,程序員在xml或者代碼里面構(gòu)建的View都在ContentView中。

從DecorView到“ContentFrameLayout[0]”的這一段Path是Android系統(tǒng)Framework層決定的,理論上應(yīng)該是一致的,但是由于碎片化等原因可能ViewPath的這一段發(fā)生變化.在實(shí)踐中,我們也發(fā)現(xiàn)確實(shí)有一些Rom發(fā)生了此類(lèi)情況,但是比率很小.
  為了屏蔽這種可能造成同一個(gè)View在不同設(shè)備上產(chǎn)生ViewPath不同的情況,ViewPath的起點(diǎn)定義在ContentView比較好.如上面的ViewPath可優(yōu)化為:

ContentView/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]#mybutton

做法:
  構(gòu)造每一個(gè)ViewPath節(jié)點(diǎn)時(shí)可以取view.getId(),看看id的packageId部分是不是系統(tǒng)的(系統(tǒng)資源id以16進(jìn)制的0x01,0x00開(kāi)頭),如果是,生成ViewPath時(shí)屏蔽這段即可.


三、頁(yè)面的劃分

3.1 合理劃分頁(yè)面的重要性

頁(yè)面在Android中對(duì)應(yīng)于Activity和部分Fragment(比如很多app首頁(yè)多tab的設(shè)計(jì),若每個(gè)tab是使用Fragment實(shí)現(xiàn)的,那么這種tab一般看作一個(gè)頁(yè)面).頁(yè)面的劃分很重要,因?yàn)閮牲c(diǎn):

  1. 對(duì)于頁(yè)面,需要獲取Show/Hide兩個(gè)時(shí)機(jī),在此時(shí)機(jī)上報(bào)頁(yè)面Show/Hide事件,非頁(yè)面則不需要
  2. 頁(yè)面的劃分關(guān)系著用戶(hù)交互事件的所屬,例如,按鈕點(diǎn)擊事件上報(bào)格式如下:
事件名稱(chēng) 所屬頁(yè)面 ViewPath 其他屬性
ButtonClicked MainActivity XXX 省略

表格中的"所屬頁(yè)面"表示此次按鈕點(diǎn)擊事件發(fā)生在MainActivity中.將交互事件歸屬于頁(yè)面這樣對(duì)后面無(wú)論是進(jìn)行路徑分析還是統(tǒng)計(jì)控件點(diǎn)擊量分布都有很大的好處.

3.2 Android中的頁(yè)面

Android中通常需要看作頁(yè)面的有Activity和Fragment(對(duì)于像全屏Dialog或者全屏的View暫不考慮).對(duì)于Activity,上節(jié)中提到的兩點(diǎn)都很容易辦到.

a. Activity頁(yè)面

  1. 從Application.ActivityLifecycleCallbacks的onActivityResumed/onActivityPaused這兩個(gè)回調(diào)方法就可以分別得到Activity頁(yè)面Show/Hide的時(shí)機(jī),并在此時(shí)機(jī)上報(bào)相應(yīng)頁(yè)面事件
  2. 交互歸屬的Activity頁(yè)面可以通過(guò)Context輕松獲得,例如上篇文章<<Android AOP之字節(jié)碼插樁>>提到,當(dāng)按鈕點(diǎn)擊時(shí),會(huì)觸發(fā)我們插樁的代碼:
Monitor.onViewClick(view)

入?yún)iew即為我們點(diǎn)擊的view,通過(guò)view.getContext()我們一般就可以得到此View所屬的Activity,偽代碼如下:

//從View中利用context獲取所屬Activity的名字
public static String getActivityName(View view) {
    Context context = view.getContext();
    if (context instanceof Activity) {
      //context本身是Activity的實(shí)例
      return context.getClass().getSimpleName().;
    } else if (context instanceof ContextWrapper) {
      //Activity有可能被系統(tǒng)"裝飾",看看context.base是不是Activity
      Activity activity = getActivityFromContextWrapper((ContextWrapper) context);
      if (activity != null) {
        return activity.getClass().getSimpleName();
      } else {
        //如果從view.getContext()拿不到Activity的信息(比如view的context是Application),則返回當(dāng)前棧頂Activity的名字
        return currentActivityName;
      }
    }
    return "";
  }

b. fragment頁(yè)面
  相對(duì)于Activity,將某些Fragment看作頁(yè)面的邏輯就要稍微復(fù)雜一些了.這里面涉及下面幾個(gè)問(wèn)題:

  • 哪些Fragment可以需要看作頁(yè)面?
      這是需要人工決策的,機(jī)器做不了這個(gè)決定.
      目前我們這個(gè)人工干預(yù)是交給用戶(hù)研究團(tuán)隊(duì),所有Fragment截圖等信息均展示在平臺(tái)上,由用研同事選擇需要看作頁(yè)面的那些,用研選擇的結(jié)果將自動(dòng)化配置到SDK中
  • 如何得到Fragment頁(yè)面的Show/Hide頁(yè)面事件?
      由于fragment使用場(chǎng)景比較多樣,單單依靠OnResume/OnPause兩個(gè)回調(diào)表示fragment Show/Hide是不準(zhǔn)確的,比如:
    場(chǎng)景一
      首頁(yè)一個(gè)Activity承載多個(gè)Fragment Tab的情況,此時(shí)tab間切換并不會(huì)觸發(fā)Fragment的OnResume/OnPause.觸發(fā)的回調(diào)函數(shù)是onHiddenChanged(boolean hidden)
    場(chǎng)景二:
      一個(gè)ViewPager承載多個(gè)頁(yè)面的Fragment時(shí)
        a.當(dāng)?shù)谝粋€(gè)Fragment1顯示時(shí),雖然第二個(gè)Fragment2此時(shí)尚未顯示,但是Fragment2的OnResume卻以及執(zhí)行,處于resumed的狀態(tài).
        b.ViewPager頁(yè)面切換OnResume/OnPause/onHiddenChanged均未觸發(fā),觸發(fā)的回調(diào)是setUserVisibleHint
      此時(shí)判斷Fragment Show/Hide應(yīng)該用setUserVisibleHint,而非OnResume/OnPause
      如前一篇文章XXX,所述,我們通過(guò)插樁的方式Hook到了fragment的如下生命周期函數(shù)用于包裝成為Show/Hide事件:
onResume()
onPause()
onHiddenChanged(boolean hidden)
setUserVisibleHint(boolean isVisibleToUser)

使用這幾個(gè)回調(diào)包裝成適用于各種情景的FragmentShow/Hide事件的偽代碼如下:

//此回調(diào)發(fā)生,則證明是場(chǎng)景一中使用情景,
  onHiddenChanged(boolean hidden) {
    hidden == true ------FragmentShow
    hidden == false------FragmentHide
  }
//場(chǎng)景二中ViewPager頁(yè)面切換時(shí)觸發(fā)Fragment的此回調(diào),
  setUserVisibleHint(boolean isVisibleToUser) {
    if (fragment.isResumed()) {//只有resumed狀態(tài)的fragment適用此情景
      isVisibleToUser == true ------FragmentShow
      isVisibleToUser == false------FragmentHide
    }
  }
//上述使用情景之外的一般場(chǎng)景
  OnResume/OnPause{
 //fragment沒(méi)有被hide,并且UserVisibleHint為可見(jiàn)的情景
    if (!fragment.isHidden() && fragment.getUserVisibleHint()) {
      OnResume ------ FragmentShow
      OnPause  ------ FragmentHide
    }
  }
  • 如何將Fragment內(nèi)部的交互歸屬到Fragment頁(yè)面,也就是說(shuō)如何在交互發(fā)生時(shí)從view實(shí)例拿到Fragment頁(yè)面的名字(像之前拿到Activity頁(yè)面名字一樣)?
      view可以通過(guò)context拿到Activity的信息,但是卻沒(méi)有途徑拿到fragment的引用。那么,當(dāng)某個(gè)View交互發(fā)生,我們又需要獲取Fragment頁(yè)面名字的情況下,我們只能事先將Fragment頁(yè)面名寫(xiě)入此View的屬性中。
      做法大致如下:
        a. 按照前一篇文章xxx里面的方法,在Fragment.OnCreateView方法的結(jié)尾插樁,拿到return的view(即為此Fragment的頂層視圖)
        b. 判斷此Fragment是否被指定為Fragment頁(yè)面,如果是,下一步
        c.遍歷以Fragment的頂層視圖為根節(jié)點(diǎn)的ViewTree, 將Fragment名設(shè)置到此ViewTree的每一個(gè)view上。設(shè)置方法如下所示:
view.setTag(0xff000001, fragmentName);

注意:View類(lèi)有兩個(gè)名為setTag的方法

public void setTag(final Object tag)

此方法,類(lèi)內(nèi)部用一Object對(duì)象存儲(chǔ)tag,protected Object mTag = null;。listAdapter中常用于設(shè)置holder。我們此處用的不是這個(gè),不會(huì)于此用法沖突

public void setTag(int key, final Object tag)

此方法,類(lèi)內(nèi)部有一稀疏數(shù)組存儲(chǔ)tag,private SparseArray<Object> mKeyedTags;
  tag的key官方推薦資源id,因此我們可以選用類(lèi)似0xff000001之類(lèi)的app用不到的資源id進(jìn)行tag存儲(chǔ)以避免沖突
    d. 當(dāng)需要使用Fragment名時(shí),如下調(diào)用即可獲得:

view.getTag(0xff000001)

3.3 頁(yè)面名組成

前面講了將交互事件(比如點(diǎn)擊事件)歸屬到某一個(gè)頁(yè)面的方法是:

在交互事件中設(shè)置一個(gè)字段,值為頁(yè)面名稱(chēng)。

頁(yè)面可以是Activity或者Activity承載的Fragment,我們的頁(yè)面名稱(chēng)組成如下:

Activity類(lèi)名[Activity別名][Fragment類(lèi)名][Fragment別名]

說(shuō)明如下:

  1. “[]”內(nèi)的組成部分是可選的,可能有可能沒(méi)有。另外,各個(gè)組成部分之間有分隔符分割。
  2. 頁(yè)面名組成中,Activity的描述(類(lèi)名/別名)是第一層,F(xiàn)ragment的描述(類(lèi)名/別名)是第二層
  3. 別名的出現(xiàn)是為了解決單純依賴(lài)類(lèi)名無(wú)法精確區(qū)分頁(yè)面的某些情況,比如:
    在某個(gè)電商應(yīng)用中,“商品詳情頁(yè)”(同一個(gè)Activity)用于展示各種商品(iphone,電視等),如果需要把“不同商品的商品詳情頁(yè)“區(qū)分成不同頁(yè)面來(lái)統(tǒng)計(jì)pv等指標(biāo)的話,需要設(shè)置別名,如:
商品詳情頁(yè)#iphone
商品詳情頁(yè)#電視

對(duì)于別名的設(shè)置,需要程序員在業(yè)務(wù)代碼里面(如Activity.OnCreate,Fragment.onCreate等)顯式設(shè)置.


四、無(wú)需埋點(diǎn)輕松收集定制的業(yè)務(wù)數(shù)據(jù)

4.1 配置示例

之前提到過(guò),數(shù)據(jù)收集SDK可以通過(guò)配置下發(fā)即時(shí)收集定制的數(shù)據(jù),那么在Android端這個(gè)是怎么做到的呢?
首先,看一下下發(fā)的配置樣例:

//第一部分:描述
PageName:MainActivity
ViewPath:DecorView/.../ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]
EventType:ViewClick
//第二部分:數(shù)據(jù)路徑(當(dāng)描述符合時(shí),按照此路徑取數(shù)據(jù))
DataPath:this.context.demoList[5]

上面例子翻譯成數(shù)據(jù)需求就是:

1. 當(dāng)頁(yè)面(MainActivity)
2. 中的控件(DecorView/.../ViewPager[0]/ButtonFragment[0]/AppCompatButton[0])
3. 發(fā)生點(diǎn)擊事件(ViewClick)時(shí)
4. 按照路徑(this.context.demoList[5])取出數(shù)據(jù)
5. 并附加到點(diǎn)擊事件上面一起上報(bào)

按照這個(gè)描述,我們還可以描述如下等等各種數(shù)據(jù)需求:

當(dāng)(某頁(yè)面)發(fā)生事件(Show)時(shí),按照路徑(xxx)取出數(shù)據(jù),并附加到頁(yè)面Show事件上面一起上報(bào)

總結(jié)下描述的組成部分,如下:

第一層 第二層 含義
描述部分 頁(yè)面 限定頁(yè)面
ViewPath 限定按鈕
EventType 限定時(shí)機(jī)(點(diǎn)擊/前臺(tái)/PageShow)
數(shù)據(jù)路徑 一種DSL,指示目標(biāo)數(shù)據(jù)在內(nèi)存中的位置(可理解為“引用路徑”)

4.2 無(wú)埋點(diǎn)收集流程

上節(jié)展示了用于無(wú)埋點(diǎn)定制業(yè)務(wù)數(shù)據(jù)收集的配置,那么SDK收到這樣的一份配置如何最終把想要的數(shù)據(jù)收集上來(lái)呢?

  • 步驟一:產(chǎn)生原始事件。比如點(diǎn)擊時(shí)收集,當(dāng)點(diǎn)擊時(shí)會(huì)觸發(fā)我們插樁的代碼,并生成原始的點(diǎn)擊事件
Monitor.onViewClick(view)
  • 步驟二:匹配配置
    在onViewClick方法中匹配下發(fā)的配置信息,看看Page,ViewPath是否與當(dāng)前view匹配,EventType是否與當(dāng)前事件類(lèi)型匹配,若匹配則進(jìn)行下一步
    注:ViewPath的匹配可以有精確匹配和模糊匹配,精確匹配時(shí)一個(gè)ViewPath精確匹配唯一一個(gè)控件.模糊匹配時(shí)一個(gè)ViewPath可匹配多個(gè)控件,例如可以用用一個(gè)ViewPath模糊匹配一個(gè)列表中的所有條目.
  • 步驟三:按照數(shù)據(jù)路徑(DataPath)逐級(jí)反射拿到目標(biāo)數(shù)據(jù),并將找到的數(shù)據(jù)附在原始的點(diǎn)擊事件上進(jìn)行上報(bào)。

4.3 數(shù)據(jù)路徑(DataPath)

上述步驟三進(jìn)行數(shù)據(jù)收集主要是按照DataPath的描述進(jìn)行(例如示例中提到的"this.context.demoList[5]"),DataPath是一種我們用于收集定制數(shù)據(jù)而定義的一種DSL.含義如下:

a. 含義

DataPath: 指向要收集的目標(biāo)數(shù)據(jù)的一條引用路徑,解析此路徑并逐級(jí)反射最終拿到目標(biāo)數(shù)據(jù).

DataPath寫(xiě)法中的一些關(guān)鍵字(符):

關(guān)鍵字(符) 含義
. 表示對(duì)象所屬關(guān)系,如:a.b 表示實(shí)例a中的字段b
.() 表示公有方法調(diào)用,如:a.b() 表示調(diào)用實(shí)例a中的方法b.注意:方法入?yún)⒖梢允荄ataPath指向的Object
[] 數(shù)組/線性表的index. 注意:此index可以是常量數(shù)字,也可以是一個(gè)DataPath指向的數(shù)字
this DataPath字符串的起點(diǎn),表示起點(diǎn)為當(dāng)前實(shí)例(當(dāng)前View)
item DataPath字符串的起點(diǎn),表示起點(diǎn)為當(dāng)前View父節(jié)點(diǎn)中AdapterView adapter中當(dāng)前條目. 常用于列表中的數(shù)據(jù)獲取
parent DataPath節(jié)點(diǎn)中的關(guān)鍵字,用于表示當(dāng)前view的parentView.效果同view.getParent(),使用此關(guān)鍵字可減少視圖引用中的反射
childAt(x) DataPath節(jié)點(diǎn)中的關(guān)鍵字,用于表示當(dāng)前view的第x個(gè)childView.效果同view.getChildAt(x),使用此關(guān)鍵字可減少視圖引用中的反射

b. 應(yīng)用示例
  下面用兩個(gè)例子說(shuō)明如何從DataPath找到目標(biāo)數(shù)據(jù).

圖4-1 DataPath示例

示例1:列表數(shù)據(jù)獲取
  上圖中顯示是一個(gè)列表,紅框中是列表的第一個(gè)條目.那么,如果我們想要在列表中條目點(diǎn)擊時(shí),將列表展示的交易品ID(或者合作方ID)等不在界面上顯示而又存在于內(nèi)存中的數(shù)據(jù)跟隨點(diǎn)擊事件上報(bào).此處DataPath該怎么寫(xiě)?

item.productId

DataPath解釋?zhuān)?/em>

  1. 起點(diǎn)定為"item",則表示從此ListView(或者RecylerView)綁定的Adapter中當(dāng)前數(shù)據(jù)item為起點(diǎn)取數(shù)據(jù).
    假設(shè)此ListView綁定的Adapter如下:
public class DemoAdapter extends BaseAdapter {
  private ArrayList<DataItem> mDataItems;
  ......
}

則此處"item"代表的就是mDataItems[x] (x表示當(dāng)前被點(diǎn)擊條目的itemId)

2."productId"是model類(lèi)DataItem中表示"交易品ID"的字段名稱(chēng).

通過(guò)DataPath獲取數(shù)據(jù):

  1. 當(dāng)?shù)趚條目被點(diǎn)擊時(shí),如果發(fā)現(xiàn)有匹配的配置,對(duì)于起點(diǎn)為"item"的DataPath,先通過(guò)view.getParent找到上層ListView實(shí)例,然后通過(guò)listView.getAdapter()獲得綁定的Adapter實(shí)例,最后通過(guò)Adapter.getItem(ListView.getPositionForView(itemView))得到數(shù)據(jù)中第x個(gè)item,即mDataItems[x]
  2. 反射獲取mDataItems[x]中的productId字段,即可得到第x個(gè)條目的"交易品ID",將此ID跟隨第x條目的點(diǎn)擊事件進(jìn)行上報(bào)即可.

實(shí)例2:界面數(shù)據(jù)獲取
  同樣時(shí)圖4-1所示,加入我們想在列表中條目點(diǎn)擊時(shí),將條目中展示的"最新價(jià)"跟隨點(diǎn)擊事件上報(bào).此處DataPath該怎么寫(xiě)?
  紅框所示ViewTree子樹(shù)如下:

圖4-2 列表Item ViewTree子樹(shù)結(jié)構(gòu)

如上圖,選中部分是列表的ItemView(RelativeLayout),可見(jiàn)"最新價(jià)"是由index為2的TextView所展示,由此可得,列表中條目點(diǎn)擊獲取"最新價(jià)"數(shù)據(jù)的DataPath如下:

this.childAt(2).mText

DataPath解釋?zhuān)?/em>

  1. 起點(diǎn)為"this",表示當(dāng)前被點(diǎn)擊的view實(shí)例(圖4-2中被選中的RelativeLayout)
  2. "childAt(2)"表示RelativeLayout.getChildAt(2),得到圖4-2中index為2的TextView
  3. "mText" 表示取出步驟2中得到TextView實(shí)例的mText字段(TextView控件顯示的文字內(nèi)容存儲(chǔ)在mText字段內(nèi))
  4. 將取出的界面上顯示的"最新價(jià)"數(shù)據(jù)添加到原始點(diǎn)擊事件中,一起上報(bào).

c. DataPath注意事項(xiàng):
1.混淆.
  由于DataPath本質(zhì)上描述的時(shí)內(nèi)存中的"引用路徑",并且按照DataPath取數(shù)據(jù)時(shí)用了反射的方法,因此DataPath應(yīng)該描述的是混淆之后的"引用路徑".
  雖然DataPath可能受到混淆的影響,但是

* 用于存儲(chǔ)數(shù)據(jù)的model類(lèi)通常是不被混淆的.如我們之前的item關(guān)鍵字直接將起點(diǎn)設(shè)置為列表?xiàng)l目的model類(lèi)對(duì)象,不受混淆影響.
* 通過(guò)關(guān)鍵字parent/childAt(x)可以在視圖的引用中不受混淆影響
* 接口的方法通常不受混淆影響.因此在DataPath中多用接口方法調(diào)用

因此開(kāi)發(fā)在配置DataPath時(shí)應(yīng)盡量用上述不被混淆影響的字段及方法.但是,如果真的用到了混淆過(guò)的字段怎么辦.我們的方案是:

數(shù)據(jù)報(bào)警

比如版本1上配置的DataPath "a.b",在升級(jí)新版本2后不再適用,則新版本2按照"a.b"收集時(shí)將收集不到,產(chǎn)生報(bào)警信息到后臺(tái).后臺(tái)收到大量此種信息會(huì)提醒開(kāi)發(fā)為新版本配置適用新版本的DataPath.

2.代碼變化導(dǎo)致引用路徑變化,從而致使之前配置的DataPath失效.
  與代碼中埋點(diǎn)相比,線上配置進(jìn)行收集數(shù)據(jù)與代碼的變化是并行的,無(wú)關(guān)的.這就有可能造成原有代碼修改導(dǎo)致DataPath失效.其實(shí)如果客戶(hù)端架構(gòu)設(shè)計(jì)合理,功能迭代更多是在進(jìn)行代碼的擴(kuò)展,而非修改,這種導(dǎo)致DataPath失效的情況應(yīng)該會(huì)大大降低的.
  但是無(wú)論如何:

配置的DataPath擺脫不了與版本的相關(guān)性

對(duì)于此種問(wèn)題我們依然是通過(guò)前面提到的"數(shù)據(jù)報(bào)警"進(jìn)行監(jiān)控及避免的.


五、結(jié)語(yǔ)

綜上,本文介紹了數(shù)據(jù)收集邏輯中3個(gè)比較關(guān)鍵的點(diǎn)(ViewID/Page/DataPath),結(jié)合上一篇文章的(AOP原理),Android端無(wú)埋點(diǎn)數(shù)據(jù)收集技術(shù)上比較關(guān)鍵的點(diǎn)皆以總結(jié)完畢.
  當(dāng)然實(shí)現(xiàn)SDK過(guò)程中遭遇過(guò)很多比較有意思的技術(shù)問(wèn)題,后續(xù)也會(huì)陸續(xù)進(jìn)行整理.

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

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,326評(píng)論 25 708
  • 本篇文章是基于 網(wǎng)易樂(lè)得無(wú)埋點(diǎn)數(shù)據(jù)SDK 總結(jié)而成。負(fù)責(zé)無(wú)埋點(diǎn)數(shù)據(jù)收集 SDK 的開(kāi)發(fā)已經(jīng)有半年多了,期間在組內(nèi)進(jìn)...
    zerygao閱讀 48,846評(píng)論 121 388
  • 版權(quán)歸屬于微信公眾號(hào)文章網(wǎng)易HubbleData之Android無(wú)埋點(diǎn)實(shí)踐文末有彩蛋哦? 1 背景 網(wǎng)易Hubbl...
    nailperry閱讀 8,404評(píng)論 3 31
  • 苦蕎(墾荒人苦蕎麥茶) 含蘆丁等,三降食品,降血壓,降血糖,降血脂;預(yù)防、治療心血管疾病;增強(qiáng)免疫力;抗氧化作用。...
    挽箏閱讀 471評(píng)論 0 0
  • sonar 發(fā)信配置 用戶(hù)訂閱郵件通知 指派bug給用戶(hù)test02 用戶(hù)test02的郵箱收到一封bug變動(dòng)郵件...
    ktide閱讀 7,795評(píng)論 1 1

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