为了更好的拿到用户的操作数据,操作习惯,线上的错误日志,为了能在出现问题时能更快,更准的找到问题,解决问题
1.第一类是代码埋点
即在需要埋点的节点调用接口直接上传埋点数据,友盟、百度统计等第三方数据统计服务商大都采用这种方案
2.第二类是可视化埋点
即通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据,从而实现所谓的“无痕埋点”
3.第三类是“无埋点”
它并不是真正的不需要埋点,而是前端自动采集全部事件并上报埋点数据,在后端数据计算时过滤出有用数据
1.错误日志
系统默认的异常处理在类 UncaughtExceptionHandler 的uncaughtException()方法内,所以我们新建CrashHandler 实现UncaughtExceptionHandler
在其uncaughtException方法中获取异常信息
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler(); //获取默认的异常处理器
捕获了一次信息后,如果系统有对这个异常做处理则交给系统处理,否则
Process.killProcess(Process.myPid());//自己杀死自己
2.设备硬件信息
在启动页面获取设备硬件信息上传即可(Build类)
3.用户的操作数据
使用无埋点监控
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中的。
参考: 应用于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点击事件的自动搜集。
具体实现:
例如,筛选出实现了
android/view/View$OnClickListener
接口的类,然后在onClick(Landroid/view/View;)V
方法中注入采集数据的代码。
目标效果:
Fragment生命周期追踪
在ViewID优化中,我们讲到Fragment节点的优化时,提到可通过重写Fragment的几个与生命周期相关的函数监听Fragment生命周期。
这个过程除了使用代码埋点,也可借助插件自动完成:扫描class文件,定位Fragment的几个与生命周期相关的函数,自动插入代码。
目标函数(方法):
具体实现:
对app中指定包进行扫描,筛选出所有父类为下列其中之一的子类。以下是Fragment及系统内置的几个常见的Fragment派生类。
对这些Fragment子类的onResumed
,onPaused
,onHiddenChanged
,setFragmentUserVisibleHint
方法的字节码进行修改,添加数据采集代码。
目标效果:
通过无埋点搜集的数据也仅限控件的一些固有属性,并没有搜集到更有价值的业务数据。