天天看點

用戶端無埋點異常監控-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

                                            簡單執行個體如下:

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

        1.3 ViewID優化

             考慮到在實際布局中有可能存在一些動态插入、删除的控件,或者說控件被複用,都可能引起View Path的變化,進而導緻ViewID不唯一。

        為了保證ViewID的一緻性,我們從以下幾個方面着手,對ViewID進行了一定程度地優化。

1.3.1.index

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

          如上圖所示,當頁面布局發生動态變化時,比如說删除一個子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不受影響。

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

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]

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

         如何擷取Fragment執行個體?

         采用代碼埋點或後續即将講到的插件埋點,在Fragment各執行個體類中重載下面的幾個方法,并在各方法中插入SDK提供的方法調用,進而實作Fragment生命周期監聽:

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

               通過上述調用,當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資料搜集代碼。

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

           監控哪些View?

                我們在目标View的事件響應函數中插入SDK資料搜集代碼,即可實作對該類型View的監控。例如,在Button的點選事件響應函數onClick中插入SDK資料搜集代碼後,

             當Button被點選,便會執行到onClick中的SDK資料搜集代碼,進而實作Button點選事件的自動搜集。

     具體實作:

  •  對app中指定包進行掃描,篩選出實作了目标接口的類,在目标方法中添加資料采集代碼。
例如,篩選出實作了

android/view/View$OnClickListener

接口的類,然後在

onClick(Landroid/view/View;)V

方法中注入采集資料的代碼。

     目标效果:

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

    Fragment生命周期追蹤

        在ViewID優化中,我們講到Fragment節點的優化時,提到可通過重寫Fragment的幾個與生命周期相關的函數監聽Fragment生命周期。

    這個過程除了使用代碼埋點,也可借助插件自動完成:掃描class檔案,定位Fragment的幾個與生命周期相關的函數,自動插入代碼。

    目标函數(方法):

  • onResume()V
  • onPause()V
  • setUserVisibleHint(Z)V
  • onHiddenChanged(Z)V

    具體實作:

  • 對app中指定包進行掃描,篩選出所有父類為下列其中之一的子類。以下是Fragment及系統内置的幾個常見的Fragment派生類。
用戶端無埋點異常監控-Android
  • 對這些Fragment子類的

    onResumed

    onPaused

    onHiddenChanged

    setFragmentUserVisibleHint

    方法的位元組碼進行修改,添加資料采集代碼。

    目标效果:

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

總結

             通過無埋點搜集的資料也僅限控件的一些固有屬性,并沒有搜集到更有價值的業務資料。

繼續閱讀