客户端无埋点异常监控-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方法的字节码进行修改,添加数据采集代码。

    目标效果:

               

              

 

总结

             通过无埋点搜集的数据也仅限控件的一些固有属性,并没有搜集到更有价值的业务数据。

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