Android 事件统计
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 事件统计的更多相关文章
- Android事件分发机制浅谈(一)
---恢复内容开始--- 一.是什么 我们首先要了解什么是事件分发,通俗的讲就是,当一个触摸事件发生的时候,从一个窗口到一个视图,再到一个视图,直至被消费的过程. 二.做什么 在深入学习android ...
- 通俗理解Android事件分发与消费机制
深入:Android Touch事件传递机制全面解析(从WMS到View树) 通俗理解Android事件分发与消费机制 说起Android滑动冲突,是个很常见的场景,比如SliddingMenu与Li ...
- 讲讲Android事件拦截机制
简介 什么是触摸事件?顾名思义,触摸事件就是捕获触摸屏幕后产生的事件.当点击一个按钮时,通常会产生两个或者三个事件--按钮按下,这是事件一,如果滑动几下,这是事件二,当手抬起,这是事件三.所以在And ...
- HotApp小程序统计之自定义事件统计
什么是自定义事件统计 官网:https://weixin.hotapp.cn/document 自定事件,就是自定统计任意事件的执行,灵活度最高. 用上图的云笔记说明想知道如下信息 (1)多少 ...
- android事件拦截处理机制详解
前段时间刚接触过Android手机开发,对它的事件传播机制不是很了解,虽然网上也查了相关的资料,但是总觉得理解模模糊糊,似是而非,于是自己就写个小demo测试了一下.总算搞明白了它的具体机制.写下自己 ...
- Android事件分发机制(下)
这篇文章继续讨论Android事件分发机制,首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子 ...
- Android事件分发机制(上)
Android事件分发机制这个问题不止一个人问过我,每次我的回答都显得模拟两可,是因为自己一直对这个没有很好的理解,趁现在比较闲对这个做一点总结 举个例子: 你当前有一个非常简单的项目,只有一个Act ...
- Android 事件拦截机制一种粗鄙的解释
对于Android事件拦截机制,相信对于大多数Android初学者是一个抓耳挠腮难于理解的问题.其实理解这个问题并不困难. 首先,你的明白事件拦截机制到底是怎么一回事?这里说的事件拦截机制,指的是对触 ...
- android事件分发机制
android事件分发机制,给控件设置ontouch监听事件,当ontouch返回true时,他就不会走onTouchEvent方法,要想走onTouchEvent方法只需要返回ontouch返回fa ...
随机推荐
- Paxos 实现日志复制同步(Multi-Paxos)
Paxos 实现日志复制同步 这篇文章以一种易于理解的方式来解释 Multi-Paxos 的机制. Multi-Paxos 的是为了创建日志复制 一种实现方式是用一组基础 Paxos 实例,每条记录都 ...
- spring-dwr注解整合
注解配置 1.web.xml 只需将DwrServlet换为DwrSpringServlet(包名不同) 2.dwr类 3.applicationContext.xml 4.annotationCon ...
- java打包jar,war,ear包的作用、区别
java的打包jar,war,ear包的作用,区别,打包方式. a) 作用与区别 i. jar: 通常是开发时要引用通用(JAVA)类,打成包便于存放管理 ii. war ...
- css3 3D变形 入门(一)
css3 3D.html div.oembedall-githubrepos { border: 1px solid #DDD; list-style-type: none; margin: 0 0 ...
- SQL SERVER将多行数据合并成一行(转载)
昨天遇到一个SQL Server的问题:需要写一个储存过程来处理几个表中的数据,最后问题出在我想将一个表的一个列的多行内容拼接成一行 比如表中有两列数据 : ep_classes ep_name A ...
- C#操作XML方式
前言 前一篇XML读取,现在咱们继续XML操作相关 C#中也有三种操作(增.删.改.查)XML文件方法如下: 使用 XmlDocument(DOM模式) 使用 XmlTextWriter(流模式) 使 ...
- javascript学习-对象与原型
javascript学习-对象与原型 Javascript语言是符合面向对象思想的.一般来说,面向对象思想需要满足以下三个基本要求: 封装,Javascript的对象可以自由的扩充成员变量和方法,自然 ...
- Java编程规范(一)
最近在看一本有关Java编程规范的书,书中精炼阐述了使用java语言时应该遵循的一些原则.接下来的一段时间我将在这里总结我的学习内容,也希望这一系列文章能够对有需要的人有所帮助. 不考虑任何编码规范的 ...
- C++inserter
C++的迭代器适配器中常用的有插入迭代器(Inser Iterator).流迭代器(Stream Iterator)和逆向迭代器(Reverse Iterator)等! 本文主要是介绍插入迭代器(In ...
- BZOJ 3412: [Usaco2009 Dec]Music Notes乐谱(离线处理)
这道题貌似怎么写都可以吧= =,我先读入询问然后从小到大处理就行了= = PS:水水题真的好!无!聊!但是好!欢!乐! CODE: #include<cstdio>#include< ...