简化开发开源框架

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

2020-08-29  本文已影响0人  ycl_blithe

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

一. 全埋点概述

事件类型 事件定义
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导致页面重复上报。

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. 原理

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. 上传字段

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个技术点:

2. 无法采集情况

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

4. 拓展

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

控件名 ASM判断代码
AlertDialog mInterface.conteins('android/content/DialogInterfaceOnclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;I)V' 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/ExpandableListViewOnChildClickListener')&&nameDesc=='onChildClick(Landroid/content/ExpandableListView;Landroid/view/View;IIJ)Z) mInterface.conteins('android/widget/ExpandableListViewOnGroupClickListener')&&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''
AlertDialog mInterface.conteins('android/content/DialogInterfaceOnclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;I)V' 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/ExpandableListViewOnChildClickListener')&&nameDesc=='onChildClick(Landroid/content/ExpandableListView;Landroid/view/View;IIJ)Z) 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

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. 上报事件的方案选择

2. 处理流程

3. transfrom 编译优化

上一篇 下一篇

猜你喜欢

热点阅读