目的
爲了更好的拿到用戶的操作數據,操作習慣,線上的錯誤日誌,爲了能在出現問題時能更快,更準的找到問題,解決問題
收集方式
1.第一類是代碼埋點
即在需要埋點的節點調用接口直接上傳埋點數據,友盟、百度統計等第三方數據統計服務商大都採用這種方案
2.第二類是可視化埋點
即通過可視化工具配置採集節點,在前端自動解析配置並上報埋點數據,從而實現所謂的“無痕埋點”
3.第三類是“無埋點”
它並不是真正的不需要埋點,而是前端自動採集全部事件並上報埋點數據,在後端數據計算時過濾出有用數據
收集數據
1.錯誤日誌
系統默認的異常處理在類 UncaughtExceptionHandler 的uncaughtException()方法內,所以我們新建CrashHandler 實現UncaughtExceptionHandler
在其uncaughtException方法中獲取異常信息
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler(); //獲取默認的異常處理器
捕獲了一次信息後,如果系統有對這個異常做處理則交給系統處理,否則
Process.killProcess(Process.myPid());//自己殺死自己
2.設備硬件信息
在啓動頁面獲取設備硬件信息上傳即可(Build類)
3.用戶的操作數據
使用無埋點監控
無埋點關鍵技術(參考網易HubbleData)
1.view的唯一ID
1.1 何如唯一的表示一個View
在自動收集控件數據時,需要將界面上的任何一個View與其他View區分開來。這就需要爲界面上的每一個控件分配一個唯一的ViewID。
此ViewID除了具有區分性,還需要具有一致性,即同一個View無論界面佈局如何動態變化,或者說多次進入同一頁面,此ViewID理論上保持不變。
View中可以找到的特徵信息:
Id: 靜態整數。在編譯期,aapt會生成R類,其中包含所有資源ID。
Resource Id:開發者操作控件的唯一標識。一般由開發者在佈局文件中指定android:id,通過findViewById找到View。
Class Name:View所屬的Class,例如TextView、LinearLayout、ListView、ViewPager等。
這些特徵信息中的Id如果能夠使用,是可以直接用作ViewID的,
但是,從aapt生成id的原則來看,不同版本相同的resource Id對應的整數Id 是有可能不一樣的,所以沒有辦法使用Id來唯一標識。
Resource Id是開發者定義的View標識,對於有Resource Id 的View可以說具備了唯一標識,那麼沒有Resource Id的View,我們考慮通過一個index屬性來區分,
index屬性可以取每個控件所屬父組件的index(也即每個控件是其父控件的第幾個孩子),並逐級向上遍歷找到根節點,最後形成一個View Path即可用來唯一地標識這個View
1.2 ViewID的構建
通過上述分析,我們得到一條View Path:獲取每個控件自身的ID、類名、Resource Id以及位於所屬父組件的Index等特徵信息,並逐級向上遍歷找到根節點。
並結合該View所在的頁面信息,我們得到ViewID的構造形式如下:
sha-256(page : path)
1.page: ActivityName
2.path: view在控件樹中的全路徑,按照如下形式進行拼接,其中index爲當前view所屬父組件的index,id爲編寫佈局文件時的android:id屬性值,有則拼接,且index固定爲0,無則不拼接。
parent1[index]#id/parent2[index]#id/.../view[index]#id
簡單實例如下:
1.3 ViewID優化
考慮到在實際佈局中有可能存在一些動態插入、刪除的控件,或者說控件被複用,都可能引起View Path的變化,從而導致ViewID不唯一。
爲了保證ViewID的一致性,我們從以下幾個方面着手,對ViewID進行了一定程度地優化。
1.3.1.index
如上圖所示,當頁面佈局發生動態變化時,比如說刪除一個子view,其他子view所屬父組件的index也可能會改變,爲此,我們對view所屬父組件的index進行改造,通過如下算法對index賦值:
-
每個ViewGroup下的所有View作爲一個數組,從0開始;
-
每個ViewGroup下的所有View先按照Class分類,然後再把每個類型中的數據按照數組的方式,從0開始;
-
每個ViewGroup下的所有View先按照Class分類,再確認是否有Resource Id,如果存在,則index爲0,否則index爲所屬Class類型數組下的序號。
該優化處理對所有View適用。優化後效果如下:即動態改變一些控件後,只會影響同類型的控件,其他類型控件的index不受影響,也即ViewID不受影響。
1.3.2.可複用View
使用position代替index
1.ListView → 調用getPositionForView獲得position
2.RecyclerView → 調用getChildPosition
和getChildAdapterPosition
獲取position
3.ViewPager → 調用getCurrentItem
獲取position
1.3.3.Fragment節點
Fragment節點特殊處理
針對Fragment初始化順序影響ViewID的問題,我們採用的解決方案是:
如果能夠獲取到Fragment實例的類名,則使用Fragment實例的類名替換View Path中的Fragment,並設置[index]爲特殊標記[-]。
例如:使用控件篇Tab對應的Fragment實例ControlSetFragment以及特殊標記[-]替換原View Path中的Fragment[3]
如何獲取Fragment實例?
採用代碼埋點或後續即將講到的插件埋點,在Fragment各實例類中重載下面的幾個方法,並在各方法中插入SDK提供的方法調用,從而實現Fragment生命週期監聽:
通過上述調用,當Fragment生命週期變化時,SDK能夠記錄當前活躍的所有Fragment。
當某個活躍的Fragment上的控件被點擊了,SDK構造該控件的ViewID時,會自動將該Fragment實例的類名寫入View Path。
ViewPager內嵌Fragment
這裏要說明的是,ViewPager內嵌的View不僅是可複用的,同時,由於其“懶加載”、“預加載”機制,其內嵌View的加載順序也是動態的。
特別地,當ViewPager內嵌Fragment時,按照前述對Fragment節點的處理,我們會使用Fragment實例的類名替換View Path中的Fragment,
並設置[index]爲特殊標記[-]。之所以將[index]設置爲特殊標記[-],是因爲Fragment動態加載導致index不可靠,
而ViewPager中內嵌的Fragment卻可以調用ViewPager的getCurrentItem拿到position作爲index,這種情況下,是可以將index的值添加到View Path中的。
2.無埋點的實現(gradle插件)
通過前述方案,我們可以使用ViewID唯一地標識屏幕上的控件。那麼,比如一個Button,當這個Button被點擊了,SDK又是如何捕捉到這一點擊事件,
並且拿到Button實例的呢,也就是如何實現自動埋點的呢?
原理:
試想一下我們代碼埋點的過程:首先定位到事件響應函數,例如Button的onClick函數,然後在該事件響應函數中調用SDK數據蒐集接口。
下面,我們介紹使用gradle插件自動在目標響應函數中插入SDK數據蒐集代碼,達到自動埋點的目的。
我們的gradle插件採用 Android gradle 插件提供的最新的Transform API,在Apk編譯環節中、class打包成dex之前,插入了中間環節,
調用 ASM API對class文件的字節碼進行掃描,當掃描到目標事件響應函數時,在函數頭部或尾部插入SDK數據蒐集代碼。
監控哪些View?
我們在目標View的事件響應函數中插入SDK數據蒐集代碼,即可實現對該類型View的監控。例如,在Button的點擊事件響應函數onClick中插入SDK數據蒐集代碼後,
當Button被點擊,便會執行到onClick中的SDK數據蒐集代碼,從而實現Button點擊事件的自動蒐集。
具體實現:
- 對app中指定包進行掃描,篩選出實現了目標接口的類,在目標方法中添加數據採集代碼。
例如,篩選出實現了
android/view/View$OnClickListener
接口的類,然後在onClick(Landroid/view/View;)V
方法中注入採集數據的代碼。
目標效果:
Fragment生命週期追蹤
在ViewID優化中,我們講到Fragment節點的優化時,提到可通過重寫Fragment的幾個與生命週期相關的函數監聽Fragment生命週期。
這個過程除了使用代碼埋點,也可藉助插件自動完成:掃描class文件,定位Fragment的幾個與生命週期相關的函數,自動插入代碼。
目標函數(方法):
- onResume()V
- onPause()V
- setUserVisibleHint(Z)V
- onHiddenChanged(Z)V
具體實現:
-
對app中指定包進行掃描,篩選出所有父類爲下列其中之一的子類。以下是Fragment及系統內置的幾個常見的Fragment派生類。
-
對這些Fragment子類的
onResumed
,onPaused
,onHiddenChanged
,setFragmentUserVisibleHint
方法的字節碼進行修改,添加數據採集代碼。
目標效果:
總結
通過無埋點蒐集的數據也僅限控件的一些固有屬性,並沒有蒐集到更有價值的業務數據。