前言,春节前离职,本来想的可以过一个轻松且较长点的春节,节后找工作,不曾想会遇到疫情,结果真的在老家过了一个长长的春节,五一后正式回北京开始找工作,发现招聘的公司也寥寥无几。趁着时间想的写写简书吧,一来复习之前掌握的知识点;二来提高面试的机率;三来可与同道中人互相学习学习,欢迎大家踊跃评论,多提宝贵意见。
简介
这篇文章主要实现了通过反射,Proxy动态代理拦截View的单击事件,然后在里面做一些统一操作,在这里我是使用了注解的形式保存了一些打点的数据,然后进行统一打点操作,当然你也可以做别操作,主要是开发思路。
首先看一下外层是如何使用的
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
@DotOnClick(id = 2, type = "test")
private TextView mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.button);
mButton.setOnClickListener(v -> {
//TODO 在这里写这个控件本有的处理逻辑代码
Log.d(TAG, "mButton控件被单击了");
});
//对Activity中所有标注了DotOnClick的View进行Proxy操作
StatisticsProxy.initDot(this);
}
}
注意:initDot方法要在所有控件都绑定且设置了onClickListener后执行才能达到效果
控件被单击后执行的日志效果
26849-26849 D/MainActivity: mButton控件被单击了
26849-26849 D/ProxyOnClickListenerHan: 保存打点的数据 (id=2, type=test)
接下来看代码的具体实现
1、DotOnClick
注解类,里面存放了要打点的数据,我这里只保存了两个数据,你可以根据自己的业务随意变动。
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.FIELD)
public @interface DotOnClick {
int id() default -1;
String type();
}
2、StatisticsProxy
具体实现类,首先在initDot方法中通过反射拿到Activity的所有变量,再通过getViewClickListener方法拿到view变量在Activity中设置的OnClickListener,因为View的单击事件是以私有的方式存放在里面的,所以只能通过反射才能拿到。将拿到的OnClickListener和打点的数据保存进ProxyOnClickListenerHandler中,最后通过Proxy.newProxyInstance生成新的View.OnClickListener,重新设置进View即可。
public class StatisticsProxy {
public static void initDot(Object object) {
if (object == null) {
throw new NullPointerException("object is null");
}
//通过反射获取获取类中的所有变量
Class bindClass = object.getClass();
Field[] fields = bindClass.getDeclaredFields();
if (fields == null || fields.length == 0) {
return;
}
for (Field field : fields) {
//判断当前变量是否为私有
if (Modifier.isPrivate(field.getModifiers())) {
field.setAccessible(true);
}
//判断当前变量是否标注了DotOnClick注解
if (!field.isAnnotationPresent(DotOnClick.class)) {
continue;
}
//判断当前变量是否为view的子类
if (!View.class.isAssignableFrom(field.getType())) {
continue;
}
View view;
try {
view = (View) field.get(object);
if (view == null) {
throw new NullPointerException("view is null");
}
//如果当前控件没有实现单击事件的回调监听,则抛异常
if (!view.hasOnClickListeners()) {
throw new NullPointerException("OnClickListeners is null");
}
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
//实现动态代理部分
View.OnClickListener proxyClickListener = proxyClickListener(
bindClass.getClassLoader(),
field.getAnnotation(DotOnClick.class),
getViewClickListener(view));
view.setOnClickListener(proxyClickListener);
}
}
/**
* 通过动态代理生成View.OnClickListener
*/
private static View.OnClickListener proxyClickListener(
ClassLoader classLoader,
DotOnClick dot,
View.OnClickListener listener) {
return (View.OnClickListener) Proxy.newProxyInstance(classLoader
, new Class[]{View.OnClickListener.class}
, new ProxyOnClickListenerHandler(dot, listener));
}
/**
* 通过反射获取view的onClickListener事件
*/
private static View.OnClickListener getViewClickListener(View view) {
if (view == null) {
return null;
}
try {
Class viewClass = null;
//在类的继承关系中寻找View.class,OnClickListener保存在其中
for (Class c = view.getClass(); c != null; c = c.getSuperclass()) {
if (c == View.class) {
viewClass = c;
break;
}
}
if (viewClass == null) {
return null;
}
//通过反射获取View中的mListenerInfo变量。
//注意:不同版本可能mListenerInfo变量名不一样,或者存储OnClickListener的位置不同,需要自己处理版本兼容
Field listenerField = viewClass.getDeclaredField("mListenerInfo");
listenerField.setAccessible(true);
Object listenerInfo = listenerField.get(view);
Field clickListenerField = listenerInfo.getClass().getField("mOnClickListener");
return (View.OnClickListener) clickListenerField.get(listenerInfo);
} catch (NoSuchFieldException e) {
e.printStackTrace();
return null;
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
}
关于Proxy.newProxyInstance原理这里简单描述一下,Proxy.newProxyInstance是虚拟机在运行时动态生成的Class,其继承了Proxy类,实现了newProxyInstance方法第二个参数Class数组中的所有接口,InvocationHandler也只是其里面的一个回调而已。感兴趣的同学可以通过ProxyGenerator.generateProxyClass将其全部内容导成class文件查看,这里就不做具体展示了。
3、ProxyOnClickListenerHandler
动态代理中InvocationHandler 的具体实现类,里面保存了要打点的数据与View的OnClickListener 。
public class ProxyOnClickListenerHandler implements InvocationHandler {
private static final String TAG = "ProxyOnClickListenerHan";
private DotOnClick mDot;
private View.OnClickListener mOnClickListener;
ProxyOnClickListenerHandler(DotOnClick dot, View.OnClickListener listener) {
mDot = dot;
mOnClickListener = listener;
}
/**
* 用户触发单击事件后会先调用到这里
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
if (mOnClickListener == null) {
return null;
}
Object object;
try {
object = method.invoke(mOnClickListener, args);
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
} catch (InvocationTargetException e) {
e.printStackTrace();
return null;
}
if (mDot != null) {
//TODO 在这里执行打点数据的保存
Log.d(TAG, "保存打点的数据 " + mDot.toString());
}
return object;
}
}
结尾
有任何问题还望多多指正,顺便有内推Android开发岗位的还可私信。