Android 事件统计


1.写在前面的话

最近都在看framework的东西,也几天没有写什么东西,今天有点时间写下上次面试遇到的一个问题。问题大概是这样的,如果我需要统计页面的点击事件,即添加埋点进行统计,如何实现?我当时回答的是反射加代理去实现这个功能。有朋友说,这不是很简单嘛,直接用代理模式就OK了啊,干嘛还反射。的确,如果在项目初期就确定了这个需求的话,我想大部分人都会想到用代理模式来实现这个功能。但是如果项目已经稳定运行了一段时间呢?我们不可能把每个事件都重新替换成我们的代理类吧?这样重复的工作太没有效率了,这里我们可以通过反射加代理技术来实现这个功能。


2.反射和代理

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;

在运行时判断任意一个对象所属的类;

在运行时构造任意一个类的对象;

在运行时判断任意一个类所具有的成员变量和方法;

在运行时调用任意一个对象的方法;

生成动态代理。

下面通过一个例子来讲解下反射的用途。

package com.nick.model;

//定义了一个实体类UserModel
public class UserModel {
    private String userName;
    private String password;
    private UserInfoModel userInfoModel;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public UserInfoModel getUserInfoModel() {
        return userInfoModel;
    }

    public void setUserInfoModel(UserInfoModel userInfoModel) {
        this.userInfoModel = userInfoModel;
    }

    @Override
    public String toString() {
        String result = "userName = " + userName + " password = " + password + " " + userInfoModel.toString();
        return result;
    }
}

另一个Model

package com.nick.model;

public class UserInfoModel {
    private int age;
    private String birth;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getBirth() {
        return birth;
    }

    public void setBirth(String birth) {
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "age = " + age + " birth = " + birth;
    }
}
public static void main(String[] args) {
        UserInfoModel userInfoModel = new UserInfoModel();
        userInfoModel.setAge(10);
        userInfoModel.setBirth("2017-03-17 17:08:56");
        UserModel userModel = new UserModel();
        userModel.setUserName("小红");
        userModel.setPassword("password");
        userModel.setUserInfoModel(userInfoModel);

        System.out.println(userModel.toString());

        // 通过反射修改属性
        try {
            Class userModelRe = Class.forName(UserModel.class.getName());
            Field userName = userModelRe.getDeclaredField("userName");
            userName.setAccessible(true);// setAccessible(true)的方式关闭安全检查就可以达到提升反射速度的目的
            userName.set(userModel, "小明");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        System.out.println(userModel.toString());
    }

运行结果为:运行结果

代理模式的话分为动态代理和静态代理,我们这里使用到了静态代理,这里不做过多赘述。


3. 准备工作

首先我们通过源码来看我们的点击事件是如何执行的,我们先看setOnClickListener怎么实现:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

这里很简单,就是把我们的OnClickListener赋值给listenerInfo对像的mOnClickListener。简单说下,这里进行了 isClickable() 判断,如果不可以点击,就设置为可点击。接着我们看下listenerInfo又是什么鬼:

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
    
    static class ListenerInfo {
        protected OnFocusChangeListener mOnFocusChangeListener;

        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        protected OnScrollChangeListener mOnScrollChangeListener;

        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;

        protected OnContextClickListener mOnContextClickListener;

        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
    }
    

通过源码可以看到,ListenerInfo是一些事件监听的类。那我们的OnClick又是在哪里调用的呢?

    private final class PerformClick implements Runnable {
        @Override
        public void run() {
            performClick();
        }
    }
    
    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

可以看到是用过PerformClick这个方法去调用的,那么问题来了,这个PerformClick又在哪里调用了呢?还是继续看源码:

public boolean onTouchEvent(MotionEvent event) {
       
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true, x, y);
                   }

                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;
                ...
                ...
    }

从代码里我们可以看到performClick是在onTouchEvent中的MotionEvent.ACTION_UP进行判断并执行。好像有点扯远了,回过头来我们看下应该怎样去反射获得mListenerInfo这个属性,并且获得mListenerInfo中的mOnClickListener,然后将我们的代理类赋值进去。


4.代码实现

原理上面我们都讲了,下面就是代码的实现部分:

public class HookUtils {
    private static final String VIEW_CLASS = "android.view.View";

    /**
     * @param mActivity
     * @param onClickListener
     */
    public static void hookListener(Activity mActivity, OnClickListener onClickListener) {
        if (mActivity != null) {
            View decorView = mActivity.getWindow().getDecorView();
            getView(decorView, onClickListener);
        }
    }

    /**
     * 递归进行viewHook
     * @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("mListenerInfo");//获得View属性mListenerInfo
            listenerInfoField.setAccessible(true);
            Object mListenerInfo = listenerInfoField.get(view);//ListenerInfo==>>View对象中的mListenerInfo的实例

            if (mListenerInfo != null) {
                Class listenerInfo2 = Class.forName("android.view.View$ListenerInfo");//反射创建ListenerInfo
                Field onClickListenerFiled = listenerInfo2.getDeclaredField("mOnClickListener");//获得ListenerInfo属性mOnClickListener
                onClickListenerFiled.setAccessible(true);
                View.OnClickListener o1 = (View.OnClickListener) onClickListenerFiled.get(mListenerInfo);//获得mListenerInfo的实例中的mOnClickListener实例
                if (o1 != null) {
                    View.OnClickListener onClickListenerProxy = new OnClickListenerProxy(o1, onClickListener);
                    onClickListenerFiled.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 v);
        void afterInListener(View v);
    }

    private static class OnClickListenerProxy implements View.OnClickListener {
        private View.OnClickListener object;
        private HookUtils.OnClickListener mListener;

        public OnClickListenerProxy(View.OnClickListener object, HookUtils.OnClickListener listener) {
            this.object = object;
            this.mListener = listener;
        }

        @Override
        public void onClick(View v) {
            if (mListener != null) {
                mListener.beforeInListener(v);
            }
            if (object != null) {
                object.onClick(v);
            }
            if (mListener != null) {
                mListener.afterInListener(v);
            }
        }
    }

代码里已经有很详细的注释了,这里大概解释下:我们通过反射获得了当前View的mListenerInfo这个属性,如果mListenerInfo不为空的时候,我们获得mListenerInfo中的mOnClickListener,然后将我们的代理类赋值进去。当调用onClick方法时,会先调用我们的beforeInListener之后是onClick方法,最后调用afterInListener。


5.测试

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View view = findViewById(R.id.tv_1);
        view.setTag("1");
        view.setOnClickListener(this);
        View view1 = findViewById(R.id.tv_2);
        view1.setTag("2");
        view1.setOnClickListener(this);
        View view2 = findViewById(R.id.tv_3);
        view2.setTag("3");
        view2.setOnClickListener(this);
        HookUtils.hookListener(this, this);//要在setOnxxxListener之后调用
    }

    @Override
    public void onClick(View v) {
        Log.d("fxxk", "点击id=" + v.getId() + "v===" + v.getTag().toString());
    }

    @Override
    public void beforeInListener(View v) {
        Log.d("fxxk", "点击前id=" + v.getId() + "v===" + v.getTag().toString());
    }

    @Override
    public void afterInListener(View v) {
        Log.d("fxxk", "点击后id=" + v.getId() + "v===" + v.getTag().toString());
    }

满怀期待的结果:
结果

6. 写在最后

这个代码虽然比较少,但是我这里只实现了对OnclickListener的监听,我将代码上传到GitHub,希望有时间能够将其他事件的监听也完成。下面应该是对Looper和Handler进行分析,抽空写下自己的理解。

posted @ 2017-03-18 00:03  风起云涌,勿忘本心  阅读(1335)  评论(1编辑  收藏  举报