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

Android 事件统计的更多相关文章

  1. Android事件分发机制浅谈(一)

    ---恢复内容开始--- 一.是什么 我们首先要了解什么是事件分发,通俗的讲就是,当一个触摸事件发生的时候,从一个窗口到一个视图,再到一个视图,直至被消费的过程. 二.做什么 在深入学习android ...

  2. 通俗理解Android事件分发与消费机制

    深入:Android Touch事件传递机制全面解析(从WMS到View树) 通俗理解Android事件分发与消费机制 说起Android滑动冲突,是个很常见的场景,比如SliddingMenu与Li ...

  3. 讲讲Android事件拦截机制

    简介 什么是触摸事件?顾名思义,触摸事件就是捕获触摸屏幕后产生的事件.当点击一个按钮时,通常会产生两个或者三个事件--按钮按下,这是事件一,如果滑动几下,这是事件二,当手抬起,这是事件三.所以在And ...

  4. HotApp小程序统计之自定义事件统计

    什么是自定义事件统计     官网:https://weixin.hotapp.cn/document 自定事件,就是自定统计任意事件的执行,灵活度最高. 用上图的云笔记说明想知道如下信息 (1)多少 ...

  5. android事件拦截处理机制详解

    前段时间刚接触过Android手机开发,对它的事件传播机制不是很了解,虽然网上也查了相关的资料,但是总觉得理解模模糊糊,似是而非,于是自己就写个小demo测试了一下.总算搞明白了它的具体机制.写下自己 ...

  6. Android事件分发机制(下)

    这篇文章继续讨论Android事件分发机制,首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子 ...

  7. Android事件分发机制(上)

    Android事件分发机制这个问题不止一个人问过我,每次我的回答都显得模拟两可,是因为自己一直对这个没有很好的理解,趁现在比较闲对这个做一点总结 举个例子: 你当前有一个非常简单的项目,只有一个Act ...

  8. Android 事件拦截机制一种粗鄙的解释

    对于Android事件拦截机制,相信对于大多数Android初学者是一个抓耳挠腮难于理解的问题.其实理解这个问题并不困难. 首先,你的明白事件拦截机制到底是怎么一回事?这里说的事件拦截机制,指的是对触 ...

  9. android事件分发机制

    android事件分发机制,给控件设置ontouch监听事件,当ontouch返回true时,他就不会走onTouchEvent方法,要想走onTouchEvent方法只需要返回ontouch返回fa ...

随机推荐

  1. WdatePicker 日历控件使用方法+基本常用方法

    WdatePicker 日历控件使用方法+基本常用方法,记录一下. 很好的文章. 网上转来的. 1. 跨无限级框架显示 无论你把日期控件放在哪里,你都不需要担心会被外层的iframe所遮挡进而影响客户 ...

  2. css3弹性盒子模型——回顾。

    1.box-flex属性规定框的子元素是否可伸缩其尺寸. 父元素必须要声明display:box;子元素才可以用box-flex. 语法:box-flex:value; 示例: <style&g ...

  3. vs2015 动态链接库问题

    问题: 最近要用vs2015编写一个动态链接库,生成动态链接库后,换到另一台windows下发现无法使用. 使用depends检查发现原因是缺少 MSVCP140.DLL等动态链接库. 解决: 将编译 ...

  4. java_web总结(一)

    1.struts1ajax返回值 public ActionForward preChangeAccountPwd(ActionMapping mapping, ActionForm form, Ht ...

  5. 浅谈时间复杂度- 算法衡量标准Big O

    写在前面: 今天有一场考试,考到了Big-O的知识点,考到了一道原题,原题的答案我记住了,但实际题目有一些改动导致答案有所改动,为此作者决定重新整理一下复杂度相关知识点 Efficiency and ...

  6. linux 进程间通信 之fifo

    上一篇博客已经介绍了一种进程间通信的方式,但是那只是针对于有血缘关系的进程,即父子进程间的通信,那对于没有血缘关系的进程,那要怎么通信呢?  这就要创建一个有名管道,来解决无血缘关系的进程通信, fi ...

  7. java 文件操作 读取txt文本(兄弟常开心)

    测试一下读取文本的另一种方法:该方法只使用一个类读取了文件 注意:buffer和read方法中读取指定长度的一致 package com.swust; import java.io.*; /* * 数 ...

  8. 数组&&函数数组

    数组:一次性定义多个同类型的变量,数组在 内存中存储空间必须是连续的(查询比较快)定义数组: int a[]; int[] a;分配空间: a=new int[5]; 自动为数组元素赋以默认值 a[0 ...

  9. mycat

    mycat系列: mycat系列-概述 Cobar的十个秘密之一 Cobar的十个秘密之二 Cobar的十个秘密之三 Cobar的十个秘密之四 Cobar的十个秘密之五 Cobar的十个秘密之六 Co ...

  10. git链接GitHub命令及基本操作

    Git是一款不错的代码管理工具,下面引用百科的一段话:  Git是用于Linux内核开发的版本控制工具.与CVS.Subversion一类的集中式版本控制工具不同,它采用了分布式版本库的作法,不需要服 ...