目的
為了更好的拿到使用者的操作資料,操作習慣,線上的錯誤日志,為了能在出現問題時能更快,更準的找到問題,解決問題
收集方式
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插件)
參考: 應用于Android無埋點的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
目标效果:
總結
通過無埋點搜集的資料也僅限控件的一些固有屬性,并沒有搜集到更有價值的業務資料。