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

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

一. 全埋点概述

事件类型 事件定义
AppStart 应用程序启动,包含冷启动/热启动
AppEnd 应用程序退出,包含正常退出,home按下,程序强杀/崩溃
AppViewScreen 页面浏览,包含切换Activity/Fragment
AppClick 控件点击

1. Android View 类型

序号 控件名 监听方法
1 Button,CheckedTextView,TextView,ImageButton,ImageView View.OnClickListener
2 SeekBar SeekBar.OnSeekBarChangeListener
3 TabHost TabHost.OnTabChangeListener
4 RatingBar RatingBar.OnRatingBarChangeListener
5 CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup CompoundButton.OnCheckChangeListener
6 Spinner AdapterView.OnItemSelectListener
7 MenuItem 重写 Activity的 onOptionItemSelect,onContextItemSelect
8 ListView,GridView AdapterView.OnItemSelectChangeListener
9 ExpandableListView ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener
10 Dialog DialogInterface.OnClickListener,DialogInterface.OnMultiChoiceClickListener

2. View 绑定listener方式

序号 监听方法
1 代码方式 - 直接 setOnClickListener 监听
2 xml - 中 android:onClick 绑定方法,在方法中监听
3 butterKnife - 注解方法 @OnClick(xxx) ,在方法中监听
4 lambda 方式 - setOnClickListener(v -> xxx)【aspectj不支持】
5 dataBinding - 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,TextView getText View.OnClickListener
ImageButton,ImageView getContentDescription View.OnClickListener
CheckBox,SwitchCompat,RadioButton,ToggleButton getText CompoundButton.OnCheckChangeListener
RadioGroup 获取选中的控件,在getText CompoundButton.OnCheckChangeListener
RatingBar getRating RatingBar.OnRatingBarChangeListener
SeekBar getPrgress SeekBar.OnSeekBarChangeListener
TabHost 遍历子控件,拼接文本 TabHost.OnTabChangeListener
Spinner 遍历子控件,拼接文本 AdapterView.OnItemSelectChangeListener
MenuItem getMenuText 重写 Activity的 onOptionItemSelect,onContextItemSelect
ListView,GridView getPosition AdapterView.OnItemSelectChangeListener
ExpandableListView Group position: child position ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener
Dialog getText 获取到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条件)
RatingBar 4.view是ratingBar类型
SeekBar 4.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个技术点:

  • Gradle Transfrom
    transfrom 简单api
    实例:https://github.com/wangzhzh/AutoTrackTransformProject

  • ASM
    ASM 简单api
    实例:https://github.com/wangzhzh/AutoTrackAppClick6
    实质就是在方法访问器中,方法结束里面判断接口如果实现了点击事件接口,并且方法是onclick()方法,就插入一段埋点上报的代码,达到自动埋点目的

2. 无法采集情况

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

4. 拓展

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

控件名 ASM判断代码
AlertDialog mInterface.conteins('android/content/DialogInterfaceOnMultichoiceclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;IZ)V'
MenuItem nameDesc=='onContextItemSelected(Landroid/view/MenuItem;Z)V' 或 nameDesc=='onOptionsItemSelected(Landroid/view/MenuItem;Z)V'
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup mInterface.conteins('android/widget/CompoundButton$OnCheckChangeListener')&&nameDesc=='onCheckChanged(Landroid/content/CompoundButton;Z)V'
RatingBar mInterface.conteins('android/widget/RatingBar$OnRatingBarChangeListener')&&nameDesc=='onRatingChanged(Landroid/content/RatingBar;FZ)V'
SeekBar mInterface.conteins('android/widget/SeekBar$OnSeekBarChangeListener')&&nameDesc=='onStopTrackingTouch(Landroid/content/SeekBar;)V'
Spinner mInterface.conteins('android/widget/AdapterView$OnItemSelectListener')&&nameDesc=='onItemSelected(Landroid/content/AdapterView;Landroid/view/View;IJ)V
TabHost mInterface.conteins('android/widget/TabHost$OnTabChangeListener')&&nameDesc=='onTabChanged(Ljava/lang/String;)V
ListView,GridView mInterface.conteins('android/widget/AdapterView$OnItemClickListener')&&nameDesc=='onItemClick(Landroid/content/AdapterView;Landroid/view/View;IJ)V
ExpandableListView mInterface.conteins('android/widget/ExpandableListViewOnGroupClickListener')&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z

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

1. javassist

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

  • javassist
    javassist api介绍
    实例:https://github.com/wangzhzh/AutoTrackAppClick7
    实质上也是拦截到点击事件接口,及 点击方法,然后通过ctMethod的insertAfter插入埋点代码

2. 拓展

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

控件名 javassist判断代码(nameDesc=method.name+emthod.getSignature))
xml android:onclick属性绑定的事件 新增一个注解,然后加在此xml指定的方法上。annotation== xxx && 'currentMethod.getSignature=='(Landroid/view/View;)V''
AlertDialog mInterface.conteins('android/content/DialogInterfaceOnMultichoiceclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;IZ)V'
MenuItem nameDesc=='onContextItemSelected(Landroid/view/MenuItem;Z)V' 或 nameDesc=='onOptionsItemSelected(Landroid/view/MenuItem;Z)V'
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup mInterface.conteins('android/widget/CompoundButton$OnCheckChangeListener')&&nameDesc=='onCheckChanged(Landroid/content/CompoundButton;Z)V'
RatingBar mInterface.conteins('android/widget/RatingBar$OnRatingBarChangeListener')&&nameDesc=='onRatingChanged(Landroid/content/RatingBar;FZ)V'
SeekBar mInterface.conteins('android/widget/SeekBar$OnSeekBarChangeListener')&&nameDesc=='onStopTrackingTouch(Landroid/content/SeekBar;)V'
Spinner mInterface.conteins('android/widget/AdapterView$OnItemSelectChangeListener')&&nameDesc=='onItemSelected(Landroid/content/AdapterView;Landroid/view/View;IJ)V
TabHost mInterface.conteins('android/widget/TabHost$OnTabChangeListener')&&nameDesc=='onTabChanged(Ljava/lang/String;)V
ListView,GridView mInterface.conteins('android/widget/AdapterView$OnItemClickListener')&&nameDesc=='onItemClick(Landroid/content/AdapterView;Landroid/view/View;IJ)V
ExpandableListView mInterface.conteins('android/widget/ExpandableListViewOnGroupClickListener')&&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.getName==onClick&&jcMethodDecl.getParameters==void&&jcMethodDecl.getParameters.size==1
xml android:onclick属性绑定的事件 新增一个注解,然后加在此xml指定的方法上 jcMethodDecl.getName==onClick&&jcMethodDecl.getParameters==void&&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编译速度

你可能感兴趣的:(Android全埋点解决方案读书笔记(全)与最佳方案总结)