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 ...
随机推荐
- OGG学习笔记03-单向复制简单故障处理
OGG学习笔记03-单向复制简单故障处理 环境:参考:OGG学习笔记02-单向复制配置实例 实验目的:了解OGG简单故障的基本处理思路. 1. 故障现象 故障现象:启动OGG源端的extract进程, ...
- JSON在线解析,新版本JSON在线解析
SOJSON,出了新版本的JSON在线解析,真的很好用,可以上下版本.左右版本.效果图如下.它的网址是:http://www.sojson.com/simple_json.html SOJSON集成了 ...
- 【js 编程艺术】小制作四
1. html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <t ...
- dubbox注解的一个坑
我和我同事Daniel排查的一个问题,原文是我同事Daniel写的,我做了些修改了补充. 我们dubbox的provider端有很多service开发时没有考虑到幂等问题,于是只能暂时关掉dubbo的 ...
- SpringBoot 入门教程:集成mybatis,redis
SrpingBoot相较于传统的项目具有配置简单,能快速进行开发的特点,花更少的时间在各类配置文件上,更多时间在具体业务逻辑上. SPringBoot采用纯注解方式进行配置,不喜欢xml配置的同学得仔 ...
- 《解决在Word中为汉子插入拼音及音标的问题》
说明:本人使用的是Word2007版本.以下示例都是基于本人电脑操作.如有疑问,欢迎留言交流. [1]为word中的一些文字添加拼音及音标. [2]开始为文字添加拼音及音标. 选中要添加拼音及音标的文 ...
- Hbase数据库安装
一.环境准备 1.Ubuntu14.04-server 2.ssh 3.jdk1.6 4.hbase-0.98.19-hadoop2-bin.tar.gz(下载地址http://www.apache. ...
- ICMP(网际控制报文协议)
为了更有效的提高ip数据报的成功转发和交付的效率,在网际层使用了icmp网际控制报文协议,这个协议允许主机和路由器提供差错和异常情况的报告,icmp不是高层协议,而是网际层的协议,加在ip数据报中一起 ...
- 电脑机器刷BIOS
http://www.51nb.com/forum/viewthread.php?tid=934570&page=1#pid13765036 [原创]hp笔记本刷新bios失败后真的可以恢复吗 ...
- Entity Framework+SQLite+DataBaseFirst
Entity Framework+Sqlite+DataBaseFirst 本篇主要是说明在vs中配置Sqlite,及使用Entity Framework DataBaseFirst模式. 如果没有下 ...

