关于Android大数据收集,埋点统计的详细讲解以及案例代码分析附github代码

关于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地址稍后上传附上,先把原理代码讲解一下,可以直接解决项目需求。

你可能感兴趣的:(Android)