原创不易,转载请著名出处,谢谢
事件类型 | 事件定义 |
---|---|
AppStart | 应用程序启动,包含冷启动/热启动 |
AppEnd | 应用程序退出,包含正常退出,home按下,程序强杀/崩溃 |
AppViewScreen | 页面浏览,包含切换Activity/Fragment |
AppClick | 控件点击 |
序号 | 控件名 | 监听方法 |
---|---|---|
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 |
序号 | 监听方法 |
---|---|
1 | 代码方式 - 直接 setOnClickListener 监听 |
2 | xml - 中 android:onClick 绑定方法,在方法中监听 |
3 | butterKnife - 注解方法 @OnClick(xxx) ,在方法中监听 |
4 | lambda 方式 - setOnClickListener(v -> xxx)【aspectj不支持】 |
5 | dataBinding - android:onclick =“xx:xxx” ,在指定的xxx方法中监听 |
源码:https://github.com/wangzhzh/AutoTrackAppViewScreen
6.0之后执行运行时权限回调onRequestPermissionResult 之后会再次执行onResume导致页面重复上报。
采集按照如下优先级:
源码:https://github.com/wangzhzh/AutoTrackAppStartAppEnd
AppStart : Application.registerActivityLifecycleCallbacks方法onActivityStarted中,执行上报,并通过ContentProvider+SQLite存储标记(作用是解决跨进程数据共享问题,通过ContentObserver监听新进页面,标记变化,如果在30s内,就取消上个页面退出倒计时,如果超30s,就执行AppEnd上报)
AppEnd : sdk初始化的时候创建定时器,Application.registerActivityLifecycleCallbacks方法onActivityStop时,开启定时器。30s后无新页面进入,执行上报,程序奔溃,强杀退出,下次进入页面需要补上报 AppEnd
因为程序奔溃,强杀,后面需要补上报 AppEnd,如果用户后面不在使用程序,或卸载程序,会导致 AppEnd 丢失
源码:https://github.com/wangzhzh/AutoTrackAppClick1
在Application.registerActivityLifecycleCallbacks方法onActivityResume中,通过activity.getwindow.getDecorView获取到其rootView,然后递归遍历所有子控件,并对所有子控件的点击事件设置代理拦截wrapperOnClickListener,其中有无点击事件,通过反射View里面的mOnClickListener属性判断。
注意:
控件名 | 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 |
源码:https://github.com/wangzhzh/AutoTrackAppClick2
Application.registerActivityLifecycleCallbacks方法onActivityCreate中,通过activity.getWindow.getcallBack,然后设置代理wrapperWindowCallback,通过这个代理类的dispatchTouchEvent,确定点击的位置,然后从控件列表集合中找到具体的控件,插入埋点代码。
判断控件是否是集合中的哪个控件,需要满足的条件:
控件名 | 判断规则(默认满足上面1,2,3条件) |
---|---|
RatingBar | 4.view是ratingBar类型 |
SeekBar | 4.view是SeekBar类型 |
Spinner | 采用代理方式处理,代理 dapterView.OnItemSelectChangeListener |
ListView,GridView | 采用代理方式处理,代理 ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener |
源码:https://github.com/wangzhzh/AutoTrackAppClick3
在Application.registerActivityLifecycleCallbacks方法onActivityResume中,通过activity.getwindow.getDecorView获取到其rootView,然后递归遍历所有子控件,并对所有子控件设置代理拦截mAccessibilityEvent,埋点代码就在其回调方法中处理。
ratingBar/SeekBar/Spinner/ListView,GradView/ExpandableListView 均与之前《第四章View.OnClickenerListener》反射+动态代理方案一致
源码:https://github.com/wangzhzh/AutoTrackAppClick4
在activity的最上层添加一个透明的View,然后重写透明view的onTouchEvent,从里面取出xy位置,判断控件集合的具体控件,然后使用wrapperOnClickListener代理其mOnclickListener对象,并在代理类中实现埋点上报。
透明层条件:
判断控件是否在控件集合中,与之前《第六章View.AccessibilityDelegate》的寻找方法一致
与《第五章 Window.CallBack》方案一致
源码:https://github.com/wangzhzh/AutoTrackAppClick5
AOP 面向切面编程,可实现的有日志埋点,性能监控,动态权限控制,代码调试
Aspectj 使用ajc编译器,在编译期把代码插入目标程序中
Aspectj简单使用:Aspectj简单使用
使用AspectJ的2种方式:
通过给控件setTag(int,object)的方式支持拓展,后续从view中取出这个值使用,但是为了保证tag的key不重复,需要在xml中定义资源id,使用时就使用它即可
无法采集的情况 | 解决思路 | 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语法,所以无法解决 |
控件名 | 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))”) |
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()方法,就插入一段埋点上报的代码,达到自动埋点目的
无法采集的情况 | 解决思路 | ASM代码 |
---|---|---|
xml android:onclick属性绑定的事件 | 新增一个注解,然后加在此xml指定的方法上,继续visitorAnnotation中找到此注解,设置标识,并在此方法结束之后插入埋点代码 | isFlag=true&&desc==’(Landroid/view/View;)V’ |
所有的操作都是在方法访问器,结束方法中判断是否达到条件,满足则加入埋点字节码
控件名 | ASM判断代码 |
---|---|
AlertDialog | mInterface.conteins(‘android/content/DialogInterfaceKaTeX parse error: Expected 'EOF', got '&' at position 18: …clickListener')&̲&nameDesc=='onC…OnMultichoiceclickListener’)&&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/ExpandableListViewKaTeX parse error: Expected 'EOF', got '&' at position 23: …ClickListener')&̲&nameDesc=='onC…OnGroupClickListener’)&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z |
与ASM类似,为字节码操作工具。那么处理流程也是通过transfrom遍历文件找到指定类,然后通过 javassist处理指定文件,实现代码注入。
所有的操作都是在获取到所有接口数组,遍历方法,断是否达到条件,满足则通过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/DialogInterfaceKaTeX parse error: Expected 'EOF', got '&' at position 18: …clickListener')&̲&nameDesc=='onC…OnMultichoiceclickListener’)&&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/ExpandableListViewKaTeX parse error: Expected 'EOF', got '&' at position 23: …ClickListener')&̲&nameDesc=='onC…OnGroupClickListener’)&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z |
源码:https://github.com/wangzhzh/AutoTrackAppClick8
抽象语法树,用树的形式表示源代码,源代码每个元素映射到一个节点或子树。
编译器对代码的处理流程是:JavaTxt->词语法分析->生成AST->语义分析->编译字节码,通过操作AST,达到修改源代码目的。
具体流程:
无法采集的情况 | 解决思路 | 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语法,所以无法解决 |
主要根据返回值,方法名,方法参数个数及类型判断,故封装一个公用类统一判断
控件名 | 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’ |
由于本人曾参与公司埋点SDK的研发,所以对其有一套自己的理解和感悟,总结了一种最佳的方案,其方案如下
点击事件,上报方案选择
使用asm的方案是最好,最简单的,不会影响运行时的时间,直接执行点击拦截上报
页面进入/离开事件,上报方案选择
如果是activity的页面进入离开,直接通过Application.registerActivityLifecycleCallbacks可以直接监听上报。
如果希望fragment/dialog/dialogFragment/popupwindow也可以上报,可以制作他们的基类,并在基类的进入离开,加入埋点上报代码,制作transfrom 插件,通过asm方式去替换父类,注意:(此处不仅仅是通过类访问器找到父类,简单的替换父类,还需要导入常量池修改库替换常量池里面的父类,达到构造方法也同步修改,否则修改失效)
冷热启动事件,上报方案选择
通过Application.registerActivityLifecycleCallbacks统计activity的有无的个数,统计冷热启动状态,执行上报
前台,后台事件,上报方案选择
可使用此书中的方案,开启倒计时30s,或者直接根据registerActivityLifecycleCallbacks统计当前activity的状态判断也可,根据业务而定
曝光/业务/xxx上报
直接在上报sdk中提供上报方法即可
构造上报对象:sdk需创建线程池,所有的上报都应该在线程池中执行
加入消息队列:创建handler线程,使用此线程队列保证消息的次序,待上报在线程池构造成功具体的上报对象,统一封装成消息,发送到消息队列
存入数据库:消息队列取出消息,执行插入数据库操作,并加入判断100条,执行上报,或者3分钟上报数据库的上报数据
执行上报:从数据库取出消息,执行okHttp的上报,并且处理上报成功删除数据库数据,及重试机制