Android全埋点解决方案读书笔记(全)与最佳方案总结

原创不易,转载请著名出处,谢谢

一. 全埋点概述

事件类型事件定义
AppStart应用程序启动,包含冷启动/热启动
AppEnd应用程序退出,包含正常退出,home按下,程序强杀/崩溃
AppViewScreen页面浏览,包含切换Activity/Fragment
AppClick控件点击
1. Android View 类型
序号控件名监听方法
1Button,CheckedTextView,TextView,ImageButton,ImageViewView.OnClickListener
2SeekBarSeekBar.OnSeekBarChangeListener
3TabHostTabHost.OnTabChangeListener
4RatingBarRatingBar.OnRatingBarChangeListener
5CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroupCompoundButton.OnCheckChangeListener
6SpinnerAdapterView.OnItemSelectListener
7MenuItem重写 Activity的 onOptionItemSelect,onContextItemSelect
8ListView,GridViewAdapterView.OnItemSelectChangeListener
9ExpandableListViewExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener
10DialogDialogInterface.OnClickListener,DialogInterface.OnMultiChoiceClickListener
2. View 绑定listener方式
序号监听方法
1代码方式 - 直接 setOnClickListener 监听
2xml - 中 android:onClick 绑定方法,在方法中监听
3butterKnife - 注解方法 @OnClick(xxx) ,在方法中监听
4lambda 方式 - setOnClickListener(v -> xxx)【aspectj不支持】
5dataBinding - android:onclick =“xx:xxx” ,在指定的xxx方法中监听

二. AppViewScreen 全埋点方案

源码:https://github.com/wangzhzh/AutoTrackAppViewScreen

1. Application.ActivityLifecycleCallbacks
  1. 通过此registerActivityLifecycleCallbacks里面监听到onActivityResume上报AppViewScreen数据。
  2. 上报数据有event,deviceId,properties,time。
  3. 上报数据properties包含有appName,model,os-version,app-version,maunfacturer,width,height,os,lib-version,lib,activity。
2. 权限问题 READ_CONTACTS

6.0之后执行运行时权限回调onRequestPermissionResult 之后会再次执行onResume导致页面重复上报。

  • 解决措施
    制作ignore忽略类,在onRequestPermissionResult 回调中加入addIgnoreActivity,在onStop中 移除 removeIgnoreActivity,不上报的类也可以添加进去,在上报之前判断未忽略才执行上报。
3. 页面名称采集

采集按照如下优先级:

  1. activity.getTitle
  2. sdkInt>=11 ,直接获取getToolbarTitle{activity.getActionBar.getTitle/appCompatAct.getActionBar.gettitle}
  3. activity.packageManager.activityInfo.loadLabel

三. AppStart,AppEnd 全埋点方案

源码:https://github.com/wangzhzh/AutoTrackAppStartAppEnd

1. 原理
  • AppStart : Application.registerActivityLifecycleCallbacks方法onActivityStarted中,执行上报,并通过ContentProvider+SQLite存储标记(作用是解决跨进程数据共享问题,通过ContentObserver监听新进页面,标记变化,如果在30s内,就取消上个页面退出倒计时,如果超30s,就执行AppEnd上报)

  • AppEnd : sdk初始化的时候创建定时器,Application.registerActivityLifecycleCallbacks方法onActivityStop时,开启定时器。30s后无新页面进入,执行上报,程序奔溃,强杀退出,下次进入页面需要补上报 AppEnd

2. 缺点

因为程序奔溃,强杀,后面需要补上报 AppEnd,如果用户后面不在使用程序,或卸载程序,会导致 AppEnd 丢失

四. AppClick 全埋点方案 - 1:代理 View.OnClickenerListener

源码:https://github.com/wangzhzh/AutoTrackAppClick1

1. 原理

在Application.registerActivityLifecycleCallbacks方法onActivityResume中,通过activity.getwindow.getDecorView获取到其rootView,然后递归遍历所有子控件,并对所有子控件的点击事件设置代理拦截wrapperOnClickListener,其中有无点击事件,通过反射View里面的mOnClickListener属性判断。

注意:

  1. 根据层级关系,DecorView是最顶层,子控件包含MenuItem及R.layout.content容器,所以为了能够监听到MenuItem,取最顶层DecorView,不要取R.layout.content作为rootView,(与此同时,获取text需要加MenuItem类型的判断)
  2. 为解决页面中动态添加控件问题,所以引入ViewTreeObserver.OnGlobalLayoutListener,所以此时逻辑变更了,在registerActivityLifecycleCallbacks方法onActivityCreate中创建OnGlobalLayoutListener监听器及监听器中遍历绑定所有控件,在onActivityResume添加监听,在onActivityStop中移除监听
2. 上传字段
  • element_type: view.getclass.getCanonicalName
  • element_id: 获取view的id
  • element_content: 获取view的text
  • activity: 包名+类名(通过context获取包名,如果是contextWrapper类型,需要递归获取getBaseContext,直至找到activity返回包名)
3. 拓展
控件名content获取监听方法(反射+代理)
Button,CheckedTextView,TextViewgetTextView.OnClickListener
ImageButton,ImageViewgetContentDescriptionView.OnClickListener
CheckBox,SwitchCompat,RadioButton,ToggleButtongetTextCompoundButton.OnCheckChangeListener
RadioGroup获取选中的控件,在getTextCompoundButton.OnCheckChangeListener
RatingBargetRatingRatingBar.OnRatingBarChangeListener
SeekBargetPrgressSeekBar.OnSeekBarChangeListener
TabHost遍历子控件,拼接文本TabHost.OnTabChangeListener
Spinner遍历子控件,拼接文本AdapterView.OnItemSelectChangeListener
MenuItemgetMenuText重写 Activity的 onOptionItemSelect,onContextItemSelect
ListView,GridViewgetPositionAdapterView.OnItemSelectChangeListener
ExpandableListViewGroup position: child positionExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener
DialoggetText获取到rootView之后,在遍历所有子控件,show添加/dismiss移除OnGlobalListener监听,点击代理 DialogInterface.OnClickListener,DialogInterface.OnMultiChoiceClickListener

五. AppClick 全埋点方案 - 2:代理 Window.CallBack

源码:https://github.com/wangzhzh/AutoTrackAppClick2

1. 原理

Application.registerActivityLifecycleCallbacks方法onActivityCreate中,通过activity.getWindow.getcallBack,然后设置代理wrapperWindowCallback,通过这个代理类的dispatchTouchEvent,确定点击的位置,然后从控件列表集合中找到具体的控件,插入埋点代码。

判断控件是否是集合中的哪个控件,需要满足的条件:

  1. view.visible==view.visible
  2. view.isClickable==true
  3. MotionEvent的x,y坐标必须处于view内部
2. 拓展
控件名判断规则(默认满足上面1,2,3条件)
RatingBar4.view是ratingBar类型
SeekBar4.view是SeekBar类型
Spinner采用代理方式处理,代理 dapterView.OnItemSelectChangeListener
ListView,GridView采用代理方式处理,代理 ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener

六. AppClick 全埋点方案 - 3:代理 View.AccessibilityDelegate

源码:https://github.com/wangzhzh/AutoTrackAppClick3

1. 原理

在Application.registerActivityLifecycleCallbacks方法onActivityResume中,通过activity.getwindow.getDecorView获取到其rootView,然后递归遍历所有子控件,并对所有子控件设置代理拦截mAccessibilityEvent,埋点代码就在其回调方法中处理。

2. 拓展

ratingBar/SeekBar/Spinner/ListView,GradView/ExpandableListView 均与之前《第四章View.OnClickenerListener》反射+动态代理方案一致

3. 缺点
  1. 使用反射,效率低,有版本兼容问题
  2. 需要开启辅助功能,部分Android Rom机型上可能会失效

七. AppClick 全埋点方案 - 4:透明层

源码:https://github.com/wangzhzh/AutoTrackAppClick4

1. 原理

在activity的最上层添加一个透明的View,然后重写透明view的onTouchEvent,从里面取出xy位置,判断控件集合的具体控件,然后使用wrapperOnClickListener代理其mOnclickListener对象,并在代理类中实现埋点上报。

透明层条件:

  1. width/height需是layout.MATCH_PARENT
  2. 设置透明层在最上层,view.setElevation(xxx,999f)
  3. decorView.addView(xxx)

判断控件是否在控件集合中,与之前《第六章View.AccessibilityDelegate》的寻找方法一致

2. 拓展

与《第五章 Window.CallBack》方案一致

七. AppClick 全埋点方案 - 5:Aspectj

源码:https://github.com/wangzhzh/AutoTrackAppClick5

1. Aspectj

AOP 面向切面编程,可实现的有日志埋点,性能监控,动态权限控制,代码调试
Aspectj 使用ajc编译器,在编译期把代码插入目标程序中
Aspectj简单使用:Aspectj简单使用

使用AspectJ的2种方式:

  1. 简单的配置Aspectj:https://github.com/wangzhzh/AutoTrackAspectJProject1
  2. 自定义Gradle Plugin:https://github.com/wangzhzh/AutoTrackAspectJProject2
2. 扩展View属性

通过给控件setTag(int,object)的方式支持拓展,后续从view中取出这个值使用,但是为了保证tag的key不重复,需要在xml中定义资源id,使用时就使用它即可

3. 无法采集情况
无法采集的情况解决思路aspectj代码
butterknife的onClick注解绑定的事件新增对onClick有参数情况的切入点,无参数暂不考虑@After(“execution(@butterKnife.onclick **(android.view.View))”)
xml android:onclick属性绑定的事件新增一个注解,然后加在此xml指定的方法上@After(“execution(@xxx **(android.view.View))”)
MenuItem的点击事件新增2个menuItem监听的2方法@After(“execution(@android.app.Activity.onOptionItemSelected(android.view.MenuItem))”) @After(“execution(@android.app.Activity.onContextItemSelected(android.view.MenuItem))”)
设置onclickListener使用了lambda语法aspectj暂不支持lambda语法,所以无法解决
4. 拓展
控件名aspectj代码
AlertDialog@After(“execution(@android.content.dialogInterface.onClickListener.onClick(android.content.dialogInterface,int))”) @After(“execution(@android.content.dialogInterface.onMultiChoiceClickistener.onClick(android.content.dialogInterface,int,Boolean))”)
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup@After(“execution(@android.widget.CompoundButton.OnCheckChangeListener.onCheckChanged(android.widget.CompoundButton,Boolean))”)
RatingBar@After(“execution(@android.widget.RatingBar.OnRatingBarChangeListener.onRatingChanged(android.widget.RatingBar,float,Boolean))”)
SeekBar@After(“execution(@android.widget.SeekBar.OnSeekBarChangeListener.onStopTrackingTouch(android.widget.RatingBar,float,Boolean))”)
Spinner@After(“execution(@android.widget.AdapterView.OnItemSelectListener.onItemSelected(android.widget.AdapterView,android.view.View,int,long))”)
TabHost@After(“execution(@android.widget.TabHost.OnTabChangeListener.onTabChanged(String))”)
ListView,GridView@After(“execution(@android.widget.AdapterView.OnItemSelectChangeListener.onItemClick(android.widget.AdapterView,android.view.View,int,long))”)
ExpandableListView@After(“execution(@android.widget.ExpandableListView.OnChildClickListener.onChildClick(android.widget.ExpandableListView,android.view.View,int,long))”) @After(“execution(@android.widget.ExpandableListView.OnGroupClickListener.onGroupClick(android.widget.ExpandableListView,android.view.View,int,long))”)
5. 缺点
  1. 无法织入第三方库
  2. 无法兼容Lambda语法
  3. 有兼容性问题,D8、Gradle4.X

七. AppClick 全埋点方案 - 6:ASM

1. ASM

Android gradle 1.5.0之后,提供了transfrom API ,允许第三方插件形式,在安卓打包过程中操作.class文件,遍历类,jar包等,在此过程中可再使用字节码操作工具ASM去操作,去访问具体的类,从类中读取类名,方法,属性等,然后通过字节码指令去修改原有的类(例如:访问到onClick方法,并在方法结束之前加一段埋点上报代码),然后在将修改好的类,继续执行打包task,后续apk中就有了此上报逻辑。

涉及到的2个技术点:

2. 无法采集情况
无法采集的情况解决思路ASM代码
xml android:onclick属性绑定的事件新增一个注解,然后加在此xml指定的方法上,继续visitorAnnotation中找到此注解,设置标识,并在此方法结束之后插入埋点代码isFlag=true&&desc==’(Landroid/view/View;)V’
4. 拓展

所有的操作都是在方法访问器,结束方法中判断是否达到条件,满足则加入埋点字节码

控件名ASM判断代码
AlertDialogmInterface.conteins(‘android/content/DialogInterfaceKaTeX parse error: Expected 'EOF', got '&' at position 18: …clickListener')&̲&nameDesc=='onC…OnMultichoiceclickListener’)&&nameDesc==‘onClick(Landroid/content/DialogInterface;IZ)V’
MenuItemnameDesc==‘onContextItemSelected(Landroid/view/MenuItem;Z)V’ 或 nameDesc==‘onOptionsItemSelected(Landroid/view/MenuItem;Z)V’
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroupmInterface.conteins(‘android/widget/CompoundButton$OnCheckChangeListener’)&&nameDesc==‘onCheckChanged(Landroid/content/CompoundButton;Z)V’
RatingBarmInterface.conteins(‘android/widget/RatingBar$OnRatingBarChangeListener’)&&nameDesc==‘onRatingChanged(Landroid/content/RatingBar;FZ)V’
SeekBarmInterface.conteins(‘android/widget/SeekBar$OnSeekBarChangeListener’)&&nameDesc==‘onStopTrackingTouch(Landroid/content/SeekBar;)V’
SpinnermInterface.conteins(‘android/widget/AdapterView$OnItemSelectListener’)&&nameDesc=='onItemSelected(Landroid/content/AdapterView;Landroid/view/View;IJ)V
TabHostmInterface.conteins(‘android/widget/TabHost$OnTabChangeListener’)&&nameDesc=='onTabChanged(Ljava/lang/String;)V
ListView,GridViewmInterface.conteins(‘android/widget/AdapterView$OnItemClickListener’)&&nameDesc=='onItemClick(Landroid/content/AdapterView;Landroid/view/View;IJ)V
ExpandableListViewmInterface.conteins(‘android/widget/ExpandableListViewKaTeX parse error: Expected 'EOF', got '&' at position 23: …ClickListener')&̲&nameDesc=='onC…OnGroupClickListener’)&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z

七. AppClick 全埋点方案 - 7:Javassist

1. javassist

与ASM类似,为字节码操作工具。那么处理流程也是通过transfrom遍历文件找到指定类,然后通过 javassist处理指定文件,实现代码注入。

2. 拓展

所有的操作都是在获取到所有接口数组,遍历方法,断是否达到条件,满足则通过method.insertAfter加入埋点字节码

控件名javassist判断代码(nameDesc=method.name+emthod.getSignature))
xml android:onclick属性绑定的事件新增一个注解,然后加在此xml指定的方法上。annotation== xxx && ‘currentMethod.getSignature==’(Landroid/view/View;)V’’
AlertDialogmInterface.conteins(‘android/content/DialogInterfaceKaTeX parse error: Expected 'EOF', got '&' at position 18: …clickListener')&̲&nameDesc=='onC…OnMultichoiceclickListener’)&&nameDesc==‘onClick(Landroid/content/DialogInterface;IZ)V’
MenuItemnameDesc==‘onContextItemSelected(Landroid/view/MenuItem;Z)V’ 或 nameDesc==‘onOptionsItemSelected(Landroid/view/MenuItem;Z)V’
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroupmInterface.conteins(‘android/widget/CompoundButton$OnCheckChangeListener’)&&nameDesc==‘onCheckChanged(Landroid/content/CompoundButton;Z)V’
RatingBarmInterface.conteins(‘android/widget/RatingBar$OnRatingBarChangeListener’)&&nameDesc==‘onRatingChanged(Landroid/content/RatingBar;FZ)V’
SeekBarmInterface.conteins(‘android/widget/SeekBar$OnSeekBarChangeListener’)&&nameDesc==‘onStopTrackingTouch(Landroid/content/SeekBar;)V’
SpinnermInterface.conteins(‘android/widget/AdapterView$OnItemSelectChangeListener’)&&nameDesc=='onItemSelected(Landroid/content/AdapterView;Landroid/view/View;IJ)V
TabHostmInterface.conteins(‘android/widget/TabHost$OnTabChangeListener’)&&nameDesc=='onTabChanged(Ljava/lang/String;)V
ListView,GridViewmInterface.conteins(‘android/widget/AdapterView$OnItemClickListener’)&&nameDesc=='onItemClick(Landroid/content/AdapterView;Landroid/view/View;IJ)V
ExpandableListViewmInterface.conteins(‘android/widget/ExpandableListViewKaTeX parse error: Expected 'EOF', got '&' at position 23: …ClickListener')&̲&nameDesc=='onC…OnGroupClickListener’)&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z

八. AppClick 全埋点方案 - 8:AST

源码:https://github.com/wangzhzh/AutoTrackAppClick8

1. APT
  • APT
    APT 简单api
    实例:https://github.com/wangzhzh/AutoTrackAPTProject
    实质就是对页面某个控件添加注解,然后此注解生成器会编译时生成添加了注解的类的辅助埋点上报类,在registerActivityLifecycleCallbacks的创建方法会找到此注解类,然后执行上报逻辑
2. AST

抽象语法树,用树的形式表示源代码,源代码每个元素映射到一个节点或子树。

编译器对代码的处理流程是:JavaTxt->词语法分析->生成AST->语义分析->编译字节码,通过操作AST,达到修改源代码目的。

具体流程:

  1. 注解处理器的process方法
  2. element=roundEnvironment.getRootElements
  3. tree=trees.getTree(element)
  4. 自定义一个TreeTranslator,执行tree.accept(this)
  5. 在TreeTranslator的visitMethodDef找到指定方法,通过AST框架插入埋点代码
3. 无法采集情况
无法采集的情况解决思路AST代码
butterknife的onClick注解绑定的事件AST遍历注解时判断@OnClick,且方法是onClick,无返回void,参数1个jcMethodDecl.getNameonClick&&jcMethodDecl.getParametersvoid&&jcMethodDecl.getParameters.size==1
xml android:onclick属性绑定的事件新增一个注解,然后加在此xml指定的方法上jcMethodDecl.getNameonClick&&jcMethodDecl.getParametersvoid&&jcMethodDecl.getParameters.size==1
设置onclickListener使用了lambda语法AST暂不支持lambda语法,所以无法解决
4. 拓展

主要根据返回值,方法名,方法参数个数及类型判断,故封装一个公用类统一判断

控件名AST代码
AlertDialog‘onclick,void,Collections.singletonList(View),After’ ‘onclick,void,Arrays.asList(dialogInterface,int),After’ ‘onclick,void,Arrays.asList(dialogInterface,int,boolean),After’
MenuItem‘onOptionsItemSelected,boolean,Collections.singletonList(MenuItem),After’ ‘onContextItemSelected,boolean,Collections.singletonList(MenuItem),After’
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup‘onCheckedChanged.void,Arrays.asList(CompoundButton,boolean),After’
RatingBar‘onRatingChanged,vpid,Arrays.asList(RatingBar,boolean),After’
SeekBar‘onStopTrackingTouch,void,Collections.singletonList(SeekBar),After’
Spinner‘onItemSelected,void,Arrays.asList(AdapterView<?>,View,int,long),After’
TabHost'onTabChanged,void,Collections.singletonList(String),After
ListView,GridView‘onItemClick,void,Arrays.asList(AdapterView<?>,View,int,long),After’
ExpandableListView‘onGroupClick,boolean,Arrays.asList(ExpandableListView,View,int,long),Before’ ‘onChildClick,boolean,Arrays.asList(ExpandableListView,View,int,int,long),Before’
5. 缺点
  1. com.sun.tools.javac.tree APi语法晦涩,理解难度大
  2. APT无法扫描其他Module
  3. 不支持lambda语法
  4. 有返回值的方法,很难把埋点代码插入方法之后

最佳方案总结

由于本人曾参与公司埋点SDK的研发,所以对其有一套自己的理解和感悟,总结了一种最佳的方案,其方案如下

1. 上报事件的方案选择
  • 点击事件,上报方案选择
    使用asm的方案是最好,最简单的,不会影响运行时的时间,直接执行点击拦截上报

  • 页面进入/离开事件,上报方案选择
    如果是activity的页面进入离开,直接通过Application.registerActivityLifecycleCallbacks可以直接监听上报。

    如果希望fragment/dialog/dialogFragment/popupwindow也可以上报,可以制作他们的基类,并在基类的进入离开,加入埋点上报代码,制作transfrom 插件,通过asm方式去替换父类,注意:(此处不仅仅是通过类访问器找到父类,简单的替换父类,还需要导入常量池修改库替换常量池里面的父类,达到构造方法也同步修改,否则修改失效)

  • 冷热启动事件,上报方案选择
    通过Application.registerActivityLifecycleCallbacks统计activity的有无的个数,统计冷热启动状态,执行上报

  • 前台,后台事件,上报方案选择
    可使用此书中的方案,开启倒计时30s,或者直接根据registerActivityLifecycleCallbacks统计当前activity的状态判断也可,根据业务而定

  • 曝光/业务/xxx上报
    直接在上报sdk中提供上报方法即可

2. 处理流程
  • 构造上报对象:sdk需创建线程池,所有的上报都应该在线程池中执行

  • 加入消息队列:创建handler线程,使用此线程队列保证消息的次序,待上报在线程池构造成功具体的上报对象,统一封装成消息,发送到消息队列

  • 存入数据库:消息队列取出消息,执行插入数据库操作,并加入判断100条,执行上报,或者3分钟上报数据库的上报数据

  • 执行上报:从数据库取出消息,执行okHttp的上报,并且处理上报成功删除数据库数据,及重试机制

3. transfrom 编译优化
  • 开启增量编译,处理好增量编译
  • 创建线程池,在线程池中执行遍历文件,jar,提高同步编译速度
  • 设置debug不开启编译,release开启编译,类似的动态配置开关,解决不需要此编译项不让其拉低apk编译速度
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值