原创不易,转载请著名出处,谢谢
一. 全埋点概述
事件类型 | 事件定义 |
---|---|
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
- 通过此registerActivityLifecycleCallbacks里面监听到onActivityResume上报AppViewScreen数据。
- 上报数据有event,deviceId,properties,time。
- 上报数据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. 页面名称采集
采集按照如下优先级:
- activity.getTitle
- sdkInt>=11 ,直接获取getToolbarTitle{activity.getActionBar.getTitle/appCompatAct.getActionBar.gettitle}
- 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属性判断。
注意:
- 根据层级关系,DecorView是最顶层,子控件包含MenuItem及R.layout.content容器,所以为了能够监听到MenuItem,取最顶层DecorView,不要取R.layout.content作为rootView,(与此同时,获取text需要加MenuItem类型的判断)
- 为解决页面中动态添加控件问题,所以引入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,确定点击的位置,然后从控件列表集合中找到具体的控件,插入埋点代码。
判断控件是否是集合中的哪个控件,需要满足的条件:
- view.visible==view.visible
- view.isClickable==true
- 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. 缺点
- 使用反射,效率低,有版本兼容问题
- 需要开启辅助功能,部分Android Rom机型上可能会失效
七. AppClick 全埋点方案 - 4:透明层
源码:https://github.com/wangzhzh/AutoTrackAppClick4
1. 原理
在activity的最上层添加一个透明的View,然后重写透明view的onTouchEvent,从里面取出xy位置,判断控件集合的具体控件,然后使用wrapperOnClickListener代理其mOnclickListener对象,并在代理类中实现埋点上报。
透明层条件:
- width/height需是layout.MATCH_PARENT
- 设置透明层在最上层,view.setElevation(xxx,999f)
- 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种方式:
- 简单的配置Aspectj:https://github.com/wangzhzh/AutoTrackAspectJProject1
- 自定义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. 缺点
- 无法织入第三方库
- 无法兼容Lambda语法
- 有兼容性问题,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/AutoTrackTransformProjectASM
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,达到修改源代码目的。
具体流程:
- 注解处理器的process方法
- element=roundEnvironment.getRootElements
- tree=trees.getTree(element)
- 自定义一个TreeTranslator,执行tree.accept(this)
- 在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. 缺点
- com.sun.tools.javac.tree APi语法晦涩,理解难度大
- APT无法扫描其他Module
- 不支持lambda语法
- 有返回值的方法,很难把埋点代码插入方法之后
最佳方案总结
由于本人曾参与公司埋点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编译速度