51信用卡 Android自動埋點實踐

背景

隨着公司業務的發展,對業務團隊的敏捷性和創新性提出了更高的要求,而通過大數據的手段在一定程度上可以幫助我們實現這個願景,同時良好的數據分析可以也幫助我們進行更好更優的決策。對於數據本身,其處理流程主要可以歸結爲以下幾點:

  1. 數據採集
  2. 數據上報
  3. 數據存儲
  4. 數據分析
  5. 數據展示

其中所謂的數據採集是針對特定用戶行爲或事件進行捕獲、處理,這一步驟無疑是十分重要的,因爲數據採集的準確性和多樣性也會直接對後續的步驟產生影響。本文也主要是討論數據採集的幾種方式,而我們常說的『埋點』就是數據採集領域的術語,數據採集的方式也可以說是埋點的幾種方式。

現狀、痛點

目前公司內部主要使用代碼埋點的方式進行數據採集,所謂代碼埋點指的是,在某個事件發生時通過預先寫好的代碼來發送數據。

基於預先編碼實現的代碼埋點,其優點是:控制精準、採集靈活性強,可以自由的選擇什麼時候發送什麼樣的數據;但缺點也同樣十分明顯,開發、測試成本高,對於客戶端而言需要等待發版才能修改線上的埋點。

日常的開發過程中,經常有同事反饋埋點的錯埋及漏埋,其根本原因都是代碼埋點本身特點導致,這樣的情況推動着我們去嘗試使用其他埋點方式。

業內情況

  • 無痕埋點,也可稱爲無埋點或者全埋點,即在端上自動採集並上報儘可能多的數據,在計算時篩選出可用的數據。其優點是:很大程度上減少開發、測試的重複勞動,數據可以回溯並且全面;缺點是:採集信息不夠靈活,並且數據量大
  • 可視化埋點,則是通過可視化工具選擇需要收集的埋點數據,下發配置給客戶端,從而解析配置採集相應埋點的方式。其優點是:很大程度上減少開發、測試的重複勞動,數據量可控,可以在線上動態的進行埋點配置,無需等待App發版;其缺點同樣是採集信息不夠靈活,並且無法解決數據回溯的問題。

階段一:無痕埋點

分析公司常用的一些數據指標,我們發現對於大部分指標而言,我們只需要有頁面的曝光事件、控件的點擊事件等一些發送時機、內容相對固定的埋點即可,而這部分埋點,恰恰可以比較方便的使用自動埋點(相對於代碼埋點這種手動埋點來說,無痕埋點及可視化埋點均可被稱爲自動埋點)來進行採集。

相對於可視化埋點來說,無痕埋點在前期不需要可視化工具進行埋點收集,SDK開發投入較小,因此我們進行了第一步從手動埋點到無痕埋點的迭代。

無痕埋點技術實現

無痕埋點需要自動採集數據,因此針對頁面、控件等元素需要生成其ID,該ID需儘量具備『唯一性』和『穩定性』。『唯一性』非常好理解,因爲對於任意元素而言,其ID應該是與其他所有元素都不同的,這樣我們才能根據ID唯一標識出那個我們想要的元素,採集上來的數據纔是準確的,不重複的。而『穩定性』則是說,元素的ID應儘量不受版本的變動而改變,這樣後期關聯業務含義的操作纔會更加便捷。

頁面ID規則

頁面的ID較容易定義,參考上文提到的『唯一性』和『穩定性』,我們很容易就可以想到將頁面所在類的類名作爲ID。類名作爲ID,首先它是相對唯一的,除了頁面複用,不存在其他類名相同的頁面,而頁面複用的情況可以通過頁面標題名稱等方式進行規避;其次它是相對穩定的,只有在頁面類名被修改的情況下ID纔會改變,而我們日常開發的過程中,除了一些頁面重大的改版之外不會輕易修改類名。在Android中,頁面有兩種類型Activity和Fragment,Fragment可以鑲嵌在不同的Activity內,因此兩者的ID定義規則有些不同:

  • Activity,ID規則爲ActivityClassName|額外參數
  • Fragment,ID規則爲ActivityClassName[FragmentClassName]|額外參數

頁面PV、UV

有了頁面的唯一ID生成的規則,我們只需要在頁面曝光的時候,生成這個ID,然後上傳即可實現頁面的PV、UV指標。至於頁面曝光的時機,在Android開發中很容易可以找到,因爲對於Activity和Fragment而言都有標準的生命週期。針對業務中PV、UV的定義,我們可以將Activity的onResume()方法,Fragment的onResume()、setUserVisibleHint(boolean isVisibleToUser)、onHiddenChanged(boolean hidden)方法作爲曝光時機,在上述方法被回調時,調用SDK埋點方法,生成ID然後上傳埋點。

  • Activity

  • Fragment

控件ID規則

相對於頁面而言,控件的ID定義規則要更加複雜。起初我們會想到用R.id,在編譯時Android aapt會給每個寫在xml裏的控件生成一個唯一ID,但是從aapt的生成規則來看,這個ID並不是固定不變的,在資源文件發生變化的時候,id也可能會出現變化,也就是不同版本的相同控件的ID是有可能不同的。根據ID需要具備的『唯一性』和『穩定性』來看,這個ID具備『唯一性』,但『穩定性』非常差,因此這個方案不可行。

緊接着我們想到,每個界面所有的控件根據其父子關係可以繪製出頁面的視圖樹,從控件本身出發,根據控件的類名加上其所處層級的位置等特徵信息,並逐級的向上遍歷,直至找到根節點位置,這樣我們就能得到一個控件在該視圖樹中的一個控件路徑;反過來說,根據這個控件路徑,我們就能在這個視圖樹中唯一確定一個控件。下圖是一個簡單的ViewTree模型:

根據上文所述控件路徑生成規則,對於Button而言,其路徑爲:FrameLayout[0]/LinearLayout[1]/Button[0],在一個頁面中,這個路徑就可以幫我們唯一定位到這個Button,但是對於不同的頁面而言,還是存在不同的控件相同的路徑的情況,因此控件ID的生成規則應爲:『頁面ID:控件路徑』。

上文頁面ID的生成規則中我們說到,對於Android來說,頁面有Activity和Fragment兩種,因爲一個Activity可以包含不同的Fragment,所以控件如果是存在於Fragment中的,則頁面ID需要爲其所在的Fragment的頁面ID,如果不在Fragment中,則包含Activity的頁面ID即可,那麼如何能夠從控件本身的實例獲取到其所在的Activity或者Fragment。
對於Activity而言比較簡單,我們可以通過如下代碼實現:

對於Fragment則相對比較麻煩,我們只能事先將Fragment對應的頁面ID和控件本身綁定,即通過打tag的方式,在Fragment的OnViewCreated方法中,拿到Fragment容器中的根View,並打上Fragment的頁面ID,然後遍歷該View,爲其所有的子控件都打上標記,核心代碼如下:

所以當我們拿到一個View的實例時,我們先看是否能拿到這個tag對應的頁面ID,如果拿不到再去找其所屬的Activity,然後得到頁面ID,隨後根據它本身的控件路徑,拼湊出控件的ID,核心代碼如下:

控件ID的優化

基於我們上述的控件ID定義,在頁面元素不發生變動的情況下,基本能夠保證『穩定性』和『唯一性』,但是頁面元素髮送動態變化,或者不同版本之間UI進行改版的情況下,我們的控件ID就會變得不夠穩定,比如以下情況:

在插入一個FrameLayout之後,我們Button的控件路徑就變成了FrameLayout[0]/LinearLayout[2]/Button[0],與之前的ID相比,已經發生了改變,變得不那麼『穩定』了,於是我們做了以下的優化:

  • 優化1:

將兄弟節點中的位置,變成相同類型控件的位置。優化後的控件路徑爲:FrameLayout[0]/LinearLayout[1]/Button[0],即使在插入FrameLayout後,其路徑仍舊不變,相較之前會更加穩定一些。但如果插入的是LinearLayout,或者整個頁面的UI進行了重構,控件路徑依舊會發生改變。

  • 優化2:

因爲不同的系統版本或手機廠商,會對頁面的根View做一定的處理,所以我們需要屏蔽掉這種情況,對於我們而言,我們只關心我們自定義的那部分佈局,即通過setContentView傳入的佈局。我們可以通過判斷控件ID是否等於android.R.id.content來獲取我們自定義的佈局的根View,並將其作爲我們控件路徑的起點。

  • 優化3:

在Android中,除了R.id和控件路徑之外,還有一個比較常用的可以作爲控件ID的特徵信息,那就是開發者寫在佈局文件中,關聯控件的Resource ID。Resource ID是開發者自己定義的關聯View的標識,在一個頁面當中,理論上是唯一的(爲什麼說是理論上,因爲還是存在有多個相同Resource ID的情況,比如動態的add多個layout,且包含了相同的Resource ID,但這種情況非常少),並且在頁面的重構過程中,Resource ID也一般不會修改,因此用Resource ID來作爲控件ID是非常合適的。但並不是所有的控件都有Resource ID,我們可以先嚐試去獲取這個ID,假如Resource ID存在,則使用Resource ID來作爲控件ID,假如Resource ID不存在,則降級使用控件路徑作爲控件ID。核心代碼如下:

控件的點擊、長按指標

有了控件ID的生成規則,控件的點擊和長按指標我們就能很方便的進行統計,因爲在Android中,控件的點擊和長按都有非常標準的回調函數,即onClick(View v)和onLongClick(View v)方法。在回調函數中調用SDK封裝好的方法,傳入被點擊控件的View對象,通過View對象本身的特徵信息,得到這個控件的唯一ID,然後上傳埋點,即可統計出我們想要的控件相關的點擊、長按指標。

  • 點擊

  • 長按

代碼插樁

通過上文的描述,我們得到了頁面和控件的ID的定義規則,也知道了只需要在相應的回調函數中寫入SDK代碼獲得我們想要的對象,就能夠計算出我們想要的指標,那麼如何才能自動的往我們現有的工程中寫入獲得對象的代碼。

在指定的切點插入指定的代碼,這個業務場景可能很多同學都非常熟悉,我們常用AOP的方式來解決這類問題,將所有的代碼插樁邏輯集中在一個SDK內處理,這樣可以最大程度的不侵入業務。

Javassist

Javassist是一個基於字節碼操作的AOP框架,它允許開發者自由的在一個已經編譯好的類中添加新的方法,或是修改已經存在的方法。但是和其他的類似庫不同的是,Javassist並不要求開發者對字節碼方面具有多麼深入的瞭解,同樣的,它也允許開發者忽略被修改的類本身的細節和結構。一個簡單的修改方法體的例子如下:

gradle插件

Javassist需要操作已經編譯好的類,Android的打包流程從下圖可以瞭解,我們可以在Java編譯器編譯完工程代碼,.class文件轉成dex之前使用Javassist來進行我們需要的代碼插樁工作。

瞭解過gradle插件的同學可能知道,在Android Gradle Plugin 版本在1.5.0及以上,我們可以使用官方提供的最新的Transform API,在打包編譯時.class打包成dex之前對class文件進行處理。具體的自定義插件過程不在贅述,我們只需要定義一個自己的Transform,繼承系統的Transform,重寫transform方法即可。

在transform方法的第二個參數裏,我們可以獲取到工程內所有的源碼編譯出來的.class文件以及所有依賴的jar包,我們挨個遍歷所有的.class文件,以及解壓縮所有的jar包,拿到jar包內的.class文件,即可實現對所有的文件進行代碼插樁的需求,核心代碼如下:

拿到.class文件之後,我們會按照上述Javassist的工作流程進行代碼插樁:

  1. 先根據類名得到CtClass對象
  2. 再根據我們想要尋找的切入點,頁面就找onResume()方法,控件就找onClick(View view)方法
  3. 然後根據方法名和參數類型,得到CtMethod對象
  4. 調用CtMethod對象的編輯方法體的API,在原始方法體之前插入就調用insertBefore,之後就調用insertAfter,傳入需要插入的代碼塊
  5. 調用CtClass的writeFile()方法,保存這次編輯

將項目中所有的源文件遍歷一邊後,我們就完成了整個項目代碼的插樁,在我們想要的切入點(頁面的曝光、控件的點擊等回調函數),就成功的插入了相應捕獲頁面、控件對象的代碼,在頁面曝光或者控件點擊時,就能夠獲得相應的對象,生成唯一ID並上報相應的埋點事件,完成整一個無痕埋點的流程了。

階段二:可視化管理後臺

完成階段一的無痕埋點之後,我們可以通過接入一個SDK來輕鬆的實現頁面曝光、控件點擊等指標的數據獲取,但是通過上文我們可以知道,我們定義的ID其實對於業務方(產品、運營、BI等非業務開發人員)而言是不友好的,他們無法根據ID中的類名、Resource ID等特徵信息來關聯到埋點具體的業務含義,因此我們需要通過一些工具來幫助他們將埋點元素ID和具體的業務含義進行關聯,甚至是跨平臺(Android、iOS的自動埋點ID是不一致的)的關聯。

從另外一個角度來說,有了這樣的可視化管理後臺,我們還可以通過下發配置表的方式來收集想要的埋點,這其實就是我們開篇說的可視化埋點。所以有了這樣的管理後臺並基於自動埋點的數據採集方式,我們可以根據具體的業務場景,靈活的選擇是無痕埋點(全量採集)還是可視化埋點(根據配置表定向採集)。

一個簡單的用戶操作可視化管理後臺的時序圖如下:

從圖中我們可以知道,可視化管理後臺的核心內容就是上傳手機界面截圖及控件相關信息,可以讓用戶在後臺對相關的頁面、控件與自定義的業務ID進行綁定並在後臺生成配置,界面實際效果如下:

在上圖的可視化管理平臺中,主要有這麼幾大塊內容,最上方是當前和管理後臺建立連接的設備信息,左下方是當前界面已經綁定過自定義業務ID的埋點元數據,右下方是手機當前界面在管理平臺上的映射,並標記出界面內所有可埋點的控件,已綁定過自定義業務ID的控件標記綠色,未綁定的標記紅色,這樣用戶就可以非常方便的選擇自己想要的控件進行操作。

要實現上圖這樣的效果,我們只需要遍歷當前頁面,並上傳所有可被埋點的控件信息,對於目前我們想要實現的數據指標而言,我們只關心控件的點擊和長按事件,換句話說就是我們只需要找到當前頁面內所有的可被點擊或長按的控件即可。

上報控件信息

對於需要上報的控件需要滿足以下幾個條件:

  1. 可被點擊或長按
  2. 在當前界面可見

對於控件是否可被點擊或長按,我們沒法直接通過系統的API來獲取,但是通過源碼我們可以看到,View內部還是有私有變量來存儲點擊或長按的監聽器的,在API14之前的mOnClickListener對象和API14之後的mListenerInfo對象,均可用來判斷當前View對象是否被設置了點擊監聽函數,我們可以通過反射來拿到這些對象,並進行判斷,長按的判斷也同理,核心代碼如下:

處理完可被點擊或長按的條件後,我們要判斷控件在當前界面是否可見,因爲我們需要在截圖上把控件全選出來,如果控件本身是不可見的也被圈出來,用戶就會比較迷茫。通過一定的調研,我們發現滿足以下幾點條件,即表示該控件在屏幕內可見:

  1. 判斷View本身可見性屬性。
    View本身可見性屬性比較容易判斷,我們只需要判斷View.isShown()並且View.getVisibility() == View.VISIBLE即可

  2. 判斷View所處的位置是否在當前屏幕內,一個Activity加載了多Fragment的情況下,可能會出現控件背身可見性屬性達標,但實際並不在屏幕內的情況。
    這種情況我們根據View.getLocationOnScreen(int[] outLocation),然後通過判斷outLocation[0],是否大於等於0且小於等於屏幕寬度,就能判斷控件是否在當前屏幕內

  3. 判斷控件是否被其他控件完全遮擋。
    遍歷所有與該控件有關聯的控件(同層控件、父控件、父控件的同層控件等),通過View.getGlobalVisibleRect(Rect viewRect)來得到控件所對應的Rect信息,然後通過Rect.contains(Rect r)來判斷兩個控件對應的Rect是否完全包含即可。

控件符合上述的可被點擊或長按且在當前界面可見這兩個條件,其信息就會被並上傳至管理後臺,用戶就可以對這個控件進行編輯,綁定自定義的業務ID,管理後臺得到控件與自定義業務ID的關聯關係後,即可生成配置表,並下發至App。這樣採集上來的埋點就會帶上自定義業務ID,用戶在後續的數據使用過程中就可以非常方便的查看相應的業務指標。

可視化管理後臺核心的邏輯就是上述的客戶端和管理後臺建立連接並上傳相應信息,其他配置的生成、下發等都非常容易處理,就不在贅述。

階段三:埋點DSL

文章開頭我們有提到過,無論是無痕埋點還是可視化埋點,都是基於自動化採集埋點的方式來做的,在這樣的採集方式下,我們無法通過埋點攜帶更多的信息,這也是我們面臨的一個痛點。基於這樣的需求之下,我們考慮可以用DSL來解決這個問題。

什麼是DSL

DSL即Domain-specific language,翻譯爲領域特定語言,意爲在特定領域解決特定任務的語言。

哪些場景下需要用到DSL

上文提到的自動埋點以頁面和控件爲切入點,hook頁面曝光和控件點擊事件,並獲取頁面及控件相關信息作爲特徵值寫入埋點。在簡單的場景下,這樣的邏輯尚可勝任,但在某些複雜的場景,比如典型的banner輪播、資源位曝光等,控件相同但實際內容不同的埋點,無法根據控件信息來區分。對於手動埋點而言,獲取接口內的信息,然後傳入埋點就能進行區分,但是自動埋點無法關聯這部分接口信息,於是需要DSL來定義簡單的規則,通過運行時的方式來獲取內存中的這部分數據,從而寫入埋點,進行更加精細的區分。

如何實現DSL

DSL的構建與編程語言其實比較類似,想想我們在重新實現編程語言時,需要做那些事情;實現編程語言的過程可以簡化爲定義語法與語義,然後實現編譯器或者解釋器的過程,而 DSL 的實現與它也非常類似,我們也需要對 DSL 進行語法與語義上的設計。總結下來,實現 DSL 總共有這麼兩個需要完成的工作:

  1. 設計語法和語義,定義 DSL 中的元素是什麼樣的,元素代表什麼意思
  2. 實現解釋器,對 DSL 解析,最終通過反射(runtime)來執行

設計語法和語義

這部分其實是千人千面的,我們可以根據自己的業務需求來不斷的迭代,但是核心思路是定義一些特殊的字符串,並對應調用各自的API,一些簡單的語法大致有以下這些:

  1. 用『.』來標識對象調用,比如『test.a』表示實例test中的a字段
  2. 用『.()』來表示方法調用,比如『test.test()』表示實例test中的test()方法調用
  3. 用『[]』來表示數組或列表

實現解釋器

說是解釋器,其實只是一段預先寫好在SDK內的代碼邏輯。通過預先約定好的語法和語義,業務開發者在可視化平臺針對某個控件進行代碼編寫,然後下發這部分代碼,SDK根據規則解析這部分代碼,然後通過反射(runtime)的方式來獲取相應的數據並寫入自動埋點。

平臺配套

可視化平臺在元素錄入的時候或者後期編輯的時候,可以額外錄入事件發生時想要獲取的數據的路徑,這部分內容需要由業務開發人員根據SDK這邊給出的規則進行路徑的錄入。成功錄入後,生成配置文件下發至App。SDK在事件發生時,獲取到相應事件攜帶的數據路徑,根據DSL約定的規則解析路徑並獲取相應的數據,存放至埋點相應字段內上傳。

總結

從最早的手動埋點到後續的無痕埋點,再到可視化管理平臺的搭建,以及DSL的實現,一步步的走來我們可以看到雖然相比手動埋點而言,自動埋點有許多優勢,但同樣其劣勢也非常明顯,即使我們通過一些工具、技術去不斷的優化和彌補它的不足,但他依舊不能完全的替代手動埋點。所以結合業務本身的特點,選擇最合適的埋點採集方式纔是最正確的做法,在一些相對穩定,不常變動的頁面、控件中使用自動埋點,可以極大的節省各個環節的時間;但如果頁面、控件本身是頻繁迭代的那自動埋點就不如手動埋點來的合適。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章