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进行分析,抽空写下自己的理解。