关于Android大数据收集,埋点统计的详细讲解以及案例代码分析附github代码
一、背景分析
目前大数据的分析对一款成熟的APP来说至关重要,特别是商业性的APP和金融类的APP都会对用户的行为进行分析,所以在APP中集成大数据的收集就显得很重要。目前来说,第三方的数据收集也挺多的,像是友盟,AOP切面收集等等,但是他们就是简单的集成,如果说在某些极端的情况下,项目中禁止添加额外的辅助,例如jar包,依赖库等等,这样我们就需要自己来手写代码了。写这篇博客之前也参考了许多网上的文章,很多是互相借鉴的,而且代码也不全,往往在我们参考思路到一半的时候就没有了,所以写此文章,来总结分析一下,本文参考了一些优秀博客,尾篇附录。
二、思路分析
一般我们进行大数据收集埋点会收集那些数据呢?一般有:点击事件的收集,下拉刷新的收集,Dialog弹出的收集,APP唤醒、挂起、启动等的收集。为了能使我们的项目达到低耦合,高内聚,以及方便我们后续的维护,所以我们写代码不能采用代码埋点的方式,也就是说哪里需要埋哪里的这种观点。所以我们就要进行封装。拿获取点击事件为例,我们想获取屏幕的点击事件,一般我们会想到监听用户的点击事件,也就是说,给控件设置上标识,我们通过监听点击事件的时候,获取到标识,根据标识在基类进行埋点,那么怎么获取到点击事件呢?同时我们又怎么能点击的时候不仅仅处理了我们的点击事件,还执行了我们的方法。这个时候我们的反射和动态代理思想就用到了。我们通过反射获取到点击事件,在通过代理在执行点击之后有执行了我们自己的方法,这样不就行了。可是问题又来了,每点击一次要遍历一遍View,还要走反射,是不是太消耗内存了,我们又会想到另外的一种方式,为什么不监听触摸事件呢,用户点击获取坐标,然后根据坐标判断位置,判断是哪个控件,不就好了。灵机一开,赶紧写代码,但是写着发现了两个问题,第一,ReecyclwerView根本获取不到条目,这样我们还不是整体封装。第二我们还是遍历了所有的ID,来判断的,最后为了解决问题,只能综合考虑,整体采用触摸事件统计的方式,采用数组,保存的方式来保存只需要遍历的ID即可。如果RecyclerView获取不到条目的埋点,只能通过设置标识来统计,这样我们传建一个基类,让需要通过反射获取埋点的来继承基类,这样,我们就不用遍历所有。
三、代码分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
View view = getCurrentFocus();
if (needHideKeyboard(view)) {
hideKeyboard(view.getWindowToken());
}
}
if (ev.getAction() == MotionEvent.ACTION_UP) {
int rawX = (int) ev.getRawX();
int rawY = (int) ev.getRawY();
for (int i = 0; i < bigDataActivityIdArrays.length; i++) {
View view = findViewById(bigDataActivityIdArrays[i]);
if (view == null) {
continue;
}
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
if (rect.contains(rawX, rawY)) {
switch (arrays[i]) { //遍历添加的数组的id
// case R.id.login:
// ToastUtils.showLongToast("Activity埋点'');
// break;
}
return super.dispatchTouchEvent(ev);
}
}
if (dispatchTouchEventTable != null) {
dispatchTouchEventTable.point(rawX, rawY);
}
if (dispatchTouchEventLoadSuccess != null) {
dispatchTouchEventLoadSuccess.loadSuccess();
}
}
return super.dispatchTouchEvent(ev);
}
private DisPathTouchEvenTable dispatchTouchEventTable;
public void setDispatchTouchEventTable(DisPathTouchEvenTable dispatchTouchEvenTable) {
this.dispatchTouchEventTable = dispatchTouchEvenTable;
}
private DispatchTouchEventLoadSuccess dispatchTouchEventLoadSuccess;
public void setDispatchTouchEventLoadSuccess(DispatchTouchEventLoadSuccess dispatchTouchEventLoadSuccess) {
this.dispatchTouchEventLoadSuccess = dispatchTouchEventLoadSuccess;
}
public interface DispatchTouchEventLoadSuccess {
void loadSuccess();
}
public interface DisPathTouchEvenTable {
void point(int x, int y);
}
添加需要埋点的id到数组里面:
/**
* 埋点记录
* 需要埋点的统计的添加到数组里面
*/
private void init() {
Arrays = new Integer[]{R.id.login};
}
如果说Activity里面没有获取到我们监听的控件,那么这时候我们会把我们触摸的坐标回调到fragment中,在fragment中我们进行判断
fragment的oncreate()中初始化我们需要埋点的控件id:
fragmentCollectIds = new Integer[]{
R.id.login,
};
然后在onActivityCreated()中进行初始化:
((BaseActivity) getActivity()).setDispatchTouchEventTable(newBaseFragmentDisPathTouchEvenTable());
进行触摸埋点统计:
private class BaseFragmentDisPathTouchEvenTable implements BaseActivity.DisPathTouchEvenTable {
@Override
public void point(int x, int y) {
for (int i = 0; i < bigDataFragmentCollectIds.length; i++) {
if (mView != null && bigDataFragmentCollectIds.length != 0) {
View viewById = mView.findViewById(bigDataFragmentCollectIds[i]);
if (viewById == null) {
continue;
}
Rect rect = new Rect();
viewById.getGlobalVisibleRect(rect);
if (rect.contains(x, y)) {
switch (bigDataFragmentCollectIds[i]) {
case R.id.login:
//登录
break;
}
}
}
}
}
}
到这一步我们的触摸埋点就全部监听到了,这样我们就可以吧除了RecyclerView中的控件的触摸全部监听的到。
接着我们讲解fragment中控件的触摸怎么收集,按照我们上面的思路我们通过反射加上代理的方式进行统计,这样贴一下封装好的工具类:
/**
* 点击事件收集类
*
* @author wangxd
* @date 2017/12/30 @time 20:09
*/
public class ClickCollectionManager {
private static final String VIEW_CLASS = "android.view.View";
private static final String ANDROID_VIEW_CLASS = "android.view.View$ListenerInfo";
private static final String LISTENER_INFO = "mListenerInfo";
private static final String ON_CLICK_LISTENER = "mOnClickListener";
/**
* @param activity
* @param onClickListener
*/
public static void hookListener(Activity activity, OnClickListener onClickListener) {
if (activity != null) {
View decorView = activity.getWindow().getDecorView();
getView(decorView, onClickListener);
}
}
/**
* 递归遍历
*
* @param view
* @param onClickListener
*/
private static void getView(View view, OnClickListener onClickListener) {
//递归遍历,判断当前view是不是ViewGroup,如果是继续遍历,直到不是为止
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
getView(((ViewGroup) view).getChildAt(i), onClickListener);
}
}
viewHook(view, onClickListener);
}
/**
* 通过反射将我们的代理类替换原来的OnClickListener
*
* @param view
* @param onClickListener
*/
private static void viewHook(View view, OnClickListener onClickListener) {
try {
Class viewClass = Class.forName(VIEW_CLASS); //创建反射View
Field listenerInfoField = viewClass.getDeclaredField(LISTENER_INFO);//获得View属性mListenerInfo
listenerInfoField.setAccessible(true);
Object mListenerInfo = listenerInfoField.get(view);//view对象中的mListenerInfo
if (mListenerInfo != null) {
Class listenerInfoClass = Class.forName(ANDROID_VIEW_CLASS);//反射创建ListenerInfo
Field onClickListenerField = listenerInfoClass.getDeclaredField(ON_CLICK_LISTENER);//获得ListenerInfo属性Onclick
onClickListenerField.setAccessible(true);
View.OnClickListener onListener = (View.OnClickListener) onClickListenerField.get(mListenerInfo);
if (onListener != null) {
View.OnClickListener onClickListenerProxy = new OnClickListenerProxy(onListener, onClickListener);
onClickListenerField.set(mListenerInfo, onClickListenerProxy);//设置ListenerInfo属性,mOnClickListener为我们的代理listener
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public interface OnClickListener {
void beforeInListener(View view);
void afterInListener(View view);
}
private static class OnClickListenerProxy implements View.OnClickListener {
private View.OnClickListener object;
private ClickCollectionManager.OnClickListener onclickListener;
public OnClickListenerProxy(View.OnClickListener object, ClickCollectionManager.OnClickListener onclickListener) {
this.object = object;
this.onclickListener = onclickListener;
}
@Override
public void onClick(View view) {
if (onclickListener != null) {
onclickListener.beforeInListener(view);
}
if (object != null) {
object.onClick(view);
}
if (onclickListener != null) {
onclickListener.afterInListener(view);
}
}
}
}
通过封装好的工具类,我们只需要把需要判断的条目设置TAG,就可以了,其实如果不考虑到性能的话我们,我们只通过这一个封装,只设置TAG就可以解决问题,但是需要注意,这里面有一个坑,fragment埋点的时候第一次点击没有效果,这就需要我们注意初始化问题了。
如果想在基类中做判断,只需要我们在基类进行封装,然后实现我们封装好的接口,这样我们不仅仅处理了点击事件,同时我们通过静态代理的方式,我们可以在点击前后进行数据的处理,这个基类封装就自行进行封装,或者直接参考demo进行封装即可。
四、挂起的监听思路以及实现
一般挂起我们指的是APP被挂载到了后台,也就是不可见不可用的状态,一般当APP进入到后台的时候,我们需要给一个友情的提示,所以对HOME键的监听就很很重要,下面看一下代码的具体封装,只需要我们集成到项目中就可以直接使用:
/**
* Home键监听
*
* @author wangxd
* @date 2017/12/30 @time 20:50
*/
public class HomeWatcher {
private Context mContext;
private IntentFilter mIntentFilter;
private OnHomePressedListener mListener;
private InnerReceiver mReceiver;
/**
* 接口回调
*/
public interface OnHomePressedListener {
/**
* 短按
*/
void onHomePressed();
/**
* 长按
*/
void onHomeLongPressed();
}
public HomeWatcher(Context context) {
mContext = context;
mIntentFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
}
/**
* 设置监听
*
* @param listener
*/
public void setOnHomePressedListener(OnHomePressedListener listener) {
mListener = listener;
mReceiver = new InnerReceiver();
}
/**
* 开始监听,注册广播
*/
public void startWatch() {
if (mReceiver != null) {
mContext.registerReceiver(mReceiver, mIntentFilter);
}
}
/**
* 停止监听,注销广播
*/
public void stopWatch() {
if (mReceiver != null) {
mContext.unregisterReceiver(mReceiver);
}
}
/**
* 广播接收者
*/
class InnerReceiver extends BroadcastReceiver {
final String SYSTEM_DIALOG_REASON_KEY = "reason";
final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
if (reason != null) {
if (mListener != null) {
if (reason.equals(SYSTEM_DIALOG_REASON_HOME_KEY)) {
mListener.onHomePressed(); // 短按home键
} else if (reason
.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) {
mListener.onHomeLongPressed();// 长按home键
}
}
}
}
}
}
}
在Activity基类中进行初始化:
/**
* home键的监听
*/
private void initHomeKeyBoardListen() {
mHomeWatcher = new HomeWatcher(this);
mHomeWatcher.setOnHomePressedListener(new HomeWatcher.OnHomePressedListener() {
@Override
public void onHomePressed() {
ToastUtils.showShortToast(getString(R.string.your_app_running_in_background));
}
@Override
public void onHomeLongPressed() {
ToastUtils.showShortToast(getString(R.string.your_app_running_in_background));
}
});
}
在生命周期中进行判断,对Home键的监听状态进行改变:
@Override
protected void onPause() {
super.onPause();
mHomeWatcher.stopWatch();
}
@Override
protected void onResume() {
super.onResume();
mHomeWatcher.startWatch()
}
五、唤醒的监听思路以及实现
唤醒一般指的是我们进行APP从后到前台的过程,目前的项目中,今日头条、条目等都进行了监听处理,一般当我们从后后台唤醒的时候,都会首先显示广告,然后在进行内容的显示,怎么显示呢,看封装好的代码:
/**
* 唤醒、首次启动
*
* @author wangxd
* @date 2017/12/30 @time 21:44
*/
public class BaseTaskSwitch i {
private BaseTaskSwitchListener taskSwitchListener;
private static BaseTaskSwitch mBaseTaskSwitch;
private static boolean mIsFirstStart = true;
public int mCount = 0;
public static BaseTaskSwitch init(Application application) {
if (null == mBaseTaskSwitch) {
mBaseTaskSwitch = new BaseTaskSwitch();
application.registerActivityLifecycleCallbacks(mBaseTaskSwitch);
}
return mBaseTaskSwitch;
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
if (mCount++ == 0) {
taskSwitchListener.onTaskSwitchToForeground();
}
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
if (--mCount == 0) {
taskSwitchListener.onTaskSwitchToBackground();
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
public interface BaseTaskSwitchListener {
void onTaskSwitchToForeground();
void onTaskSwitchToBackground();
}
public void setOnTaskSwitchListener(BaseTaskSwitchListener onTaskSwitchListener) {
this.taskSwitchListener = onTaskSwitchListener;
}
}
我们只需要在Application中进行接口的初始化即可,这样我们不仅监听到了唤醒,同时我们还监听到了第一次的启动。剩下的几种数据收集,就交给大家了。详细的代码请看github有具体的代码封装。
五、github代码demo地址
github地址稍后上传附上,先把原理代码讲解一下,可以直接解决项目需求。