客戶端無埋點異常監控-Android

目的

     爲了更好的拿到用戶的操作數據,操作習慣,線上的錯誤日誌,爲了能在出現問題時能更快,更準的找到問題,解決問題

 

收集方式

     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  → 調用getChildPositiongetChildAdapterPosition獲取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子類的onResumedonPausedonHiddenChangedsetFragmentUserVisibleHint方法的字節碼進行修改,添加數據採集代碼。

    目標效果:

               

              

 

總結

             通過無埋點蒐集的數據也僅限控件的一些固有屬性,並沒有蒐集到更有價值的業務數據。

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