制作自己的 @OnClick、@OnLongClick(告别 setOnClickListener,使用注解、反射和动态代理)
前言
前面我们说过 ButterKnife 这个库,这个库实现不仅实现了 View 的绑定,而且还提供了大量的注解如 @BindView
、@OnClick
、@OnLongClick
等来简化开发过程中事件绑定。而这些功能的实现是通过 APT 也就是注解处理器,在编译期间生成 Java 代码来实现的。
关于这一部分,大家请看前面的这篇文章,这篇文章将 ButterKnife 讲解的非常好,可以跟着一步一步实现一个自己的简易版本:
制作自己的 ButterKnife(使用 AutoService 和 APT 注解处理器在编译期生成 Java 代码)
不过这一次,我们不再使用 APT,而是利用注解和反射的巧妙结合,在运行时进行事件和 View 的绑定,让我们告别 setOnClickListener
这类的方法 。这里我们拿 @OnClick
和 @OnLongClick
这两个注解为例,自己实现这两个注解的功能。
不过想要实现自己的绑定注解还是有一些前置条件的,需要学习注解,反射和动态代理这三个方面的知识,此处推荐下面的三篇文章,建议看完,并对刚说的三个知识点有一定认识,再来看本文。
- 由浅入深 带你了解 JAVA 注解
- Java Reflection 反射使用 完全指南
- 代理模式与静态代理、动态代理的实现(Proxy.newProxyInstance、InvocationHandler)
这里插播一下我的公众号,也是最近开始弄,希望多多关注,以后我会发更多的高质量内容:
确定目标
首先我们确定要实现的是两个注解的功能:@OnClick
和 @OnLongClick
。在没有这两个注解时,我们做事件绑定的代码如下:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//为 id 为 button_1 的 View 设置 点击事件findViewById(R.id.button_1).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this, "点击", Toast.LENGTH_SHORT).show();}});//为 id 为 button_2 的 View 设置 长按事件findViewById(R.id.button_2).setOnLongClickListener(new View.OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {Toast.makeText(MainActivity.this, "长按", Toast.LENGTH_LONG).show();return true;}});}
}
而有了这两个事件绑定注解时,我们会将上面的代码改为下面这样,这也是我们最终效果的代码:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);InjectUtils.inject(this); //在此处处理此类中的时间绑定注解}@OnClick({R.id.button_1}) //为 id 为 button_1 的 View 设置 点击事件,public void click(View view) { //当 button_1 点击时,将触发此函数Toast.makeText(this, "点击", Toast.LENGTH_SHORT).show();}@OnLongClick({R.id.button_2}) //为 id 为 button_2 的 View 设置 长按事件public boolean longClick(View view) { 当 button_2 长按时,将触发此函数Toast.makeText(this, "长按", Toast.LENGTH_LONG).show();return true;}
}
确定了目标之后,我们就一步一步来实现这两个功能。这里可以看到,有三个地方需要我们去实现:
@OnClick
注解@OnLongClick
注解InjectUtils.inject(this)
代码
这三个只是我们现在看到的,如果要实现这个功能,要写的代码肯定只多不少。
实现 @OnClick
和 @OnLongClick
注解
我们先拿 @OnClick
来举个例子,正常情况下,根据要求,我们会写如下的注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {int[] value();
}
这是没问题的,Target
设置为 METHOD
表明了这个注解是放到方法上的,RUNTIME
保证了我们能够在运行时拿到这个注解,里面设置了 int[]
类型的方法,使得其可以指定多个 View
的 ID。
到现在看似没什么问题,但是这里还是有问题。想象一下后续我们会使用反射来进行 View 的事件绑定,那么在事件的绑定时,需要什么信息呢?需要的信息有两个:
- 事件绑定的方法,例如
View.setOnClickListener()
这个方法 - 设置的事件的类型,例如
View.OnClickListener
这个类型
这里说需要者两个信息大家可能会一知半解,那么先放在这里,只知道需要这两个信息就行了。等看到后面自然就明白了。
现在知道需要这两个信息,那么我们添加这两个信息不就可以了?确实是这样的,但是问题又来了,这两个信息添加到哪里呢?如果我们直接添加到 @OnClick
这个注解的方法中,就像这样:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {int[] value();String listenerSetter() default "setOnClickListener";Class<?> listenerType() default View.OnClickListener.class;
}
也不是不行,但是这样外层在用的时候,还需要指定这两个参数。这会对使用方造成误解,而且对于每一个注解,这两个信息是固定的,外面再指定是多余的,即使用 default
设置默认值,外边再重新指定这两个参数,也是我们不想看到的。
那最好的办法是怎样的呢?那就是注解多态,也就是使用自定义元注解。此处我们为 @OnClick
和 @OnLongClick
自定义一个元注解 @EventBase
。
自定义元注解 @EventBase
上面我们知道那两个额外的信息是跟注解相关的,也就是说相同的注解这两个值是相同的,这种情况,自定义一个元注解,然后来修饰这两个注解是最合适不过了。如下,我自定义了一个 @EventBase
的元注解,这个注解只能放到注解上,且两个属性值对应着上面的两个信息:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {String listenerSetter(); //绑定事件的方法,例如 "setOnClickListener"Class<?> listenerType(); //绑定的事件的类型,例如 View.OnClickListener.class
}
那么后面我们用这个注解来修改 @OnClick
和 @OnLongClick
注解。
最终的 @OnClick
注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class)
public @interface OnClick {int[] value();
}
最终的 @OnLongClick
注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnLongClickListener", listenerType = View.OnLongClickListener.class)
public @interface OnLongClick {int[] value();
}
注意这两个注解虽然都使用了 @EventBase
来修改,但是传递给 @EventBase
的两个参数是不同的,都是与自己这个事件相关的方法和参数类型。
现在,这两个注解已经确定下来了,接下来我们就要去实现 InjectUtils
这个类,而且这个类也是实现这个功能的关键所在。
利用反射和动态代理绑定事件初期器
有人会问,你这个绑定的功能为什么会起个名字叫 InjectUtils
,大家不要在意这个细节,只要知道这个类里的方法是实现 setOnClickListener
这个方法的就行,而且一般情况下,是在 MainActivity.onCreate
方法中调用的。
在最开始,我们确定了,这个类里面有 InjectUtils.inject(this)
方法,我们将会在 onCreate
中调用这个方法,就像调用 ButterKnife.bind(this)
一样。那么怎么写这个方法呢?我们一步一步来。
找到有 @OnClick
、@OnLongClick
注解的函数
既然我么要处理带有绑定注解的函数,那我们肯定就要找到有哪些函数被标记了自定义的 @OnClick
和 @OnLongClick
注解。
这一步分的代码很简单,因为我们在 MainActivity.onCreate
方法中调用了 InjectUtils.inject(this)
,而这个 this
就表示需要进行注解解析的类,通过它的 Class
对象,我们就能够找到哪些函数添加了需要处理的注解。
这部分的代码如下:
public class InjectUtils {public static void inject(Object context) {Class<?> clazz = context.getClass();Method[] methods = clazz.getDeclaredMethods();for(Method method: methods) {Annotation[] annotations = method.getAnnotations();for(Annotation annotation : annotations) {Class<? extends Annotation> annotationClass = annotation.annotationType();EventBase eventBase = annotationClass.getAnnotation(EventBase.class);//判断这个注解有没有被 @EventBase 标记,如果标记了,那么就能获取到。是我们应该处理的注解if(eventBase == null)continue;//走到这里就表示,这个方法是被 @OnClick 或 @OnLongClick 标记过的,应该处理绑定//TODO: binding}}}
}
这里主要有两个循环,外层循环是对这个类的所有方法循环,内层循环是对方法上的注解的循环。另外判断这个函数是否是事件处理的方法,则是判断注解是否被 @EventBase
注解标记,如果被这个注解标记,那么就是我们需要处理的方法了。
找到 View
并调用 setOnClickListener
接下来,走到上面 TODO
的地方,就是处理事件绑定。这里快到了全文的重点了,我们慢慢来看。
后面我们要做的其实就是调用 View.setOnClickListener(new OnClickListener())
,那么用反射完成这个调用需要分几步呢?很简单,这就像要把大象装冰箱,总共要分三步:
- 首先要找到
View
对象 - 再找到
setOnClickListener
方法 - 通过反射
invoke
这个方法
是不是很简单,下面就是代码了:
try {Method valueMethod = annotationClass.getMethod("value");int[] viewIds = (int[]) valueMethod.invoke(annotation); //获取传递给 @OnClick 的所有 View 的 IDif(viewIds == null)continue;for(int viewId: viewIds) { // 每一个 View 都要调用 setOnClickListenerMethod findViewByIdMethod = clazz.getMethod("findViewById", int.class);View view = (View) findViewByIdMethod.invoke(context, viewId); // 找到这个 viewId 对应的 View 对象if(view == null)continue;String listenerSetter = eventBase.listenerSetter();Class<?> listenerType = eventBase.listenerType();Method setListenerMethod = view.getClass().getMethod(listenerSetter, listenerType); //找到 setOnClickListener 方法setListenerMethod.invoke(view, /*TODO*/); //通过反射 invoke 这个方法}} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {exception.printStackTrace(System.err);
}
对于这一段代码,如果看了 Java Reflection 反射使用 完全指南,就很容易看懂。简单来说,就是上面的三步,这里需要注意的一点就是,获取 setOnClickListener
这个方法的地方,因为我们有两个注解,一个处理点按 @OnClick
,一个处理长按 @OnLongClick
,因此这里不能写死。
那么怎么获取对应的方法呢?还记得上面说的 @EventBase
这个注解么,这个注解就是提供这两个信息的。这两个信息是在写 @OnClick
和 @OnLongClick
注解时就传递给 @EventBase
注解的,现在大家知道了为什么还要添加 @EventBase
这个注解了吧。一是能够通过它来判断哪些是我们写的注解,二是提供了这两个信息。
这样我们就拿到了对应的 setListenerMethod
方法,那么调用它即可。不过我们在调用时会发现一个很严重的问题(即代码中的 TODO
部分):传什么参数过去?第一个参数当然是 View
对象,那第二个参数呢?是不是需要实现一个 OnClickListener
的对象?答案是否定的,这个时候,我们需要动态代理!
创建动态代理处理事件
在这里我们确实需要一个 OnClickListener
实例,但在这个地方是不可能的,我们只能使用动态代理来做这个事情。
为什么要用动态代理呢?因为这里需要代理掉 OnClickListener
中的 onClick
方法。
为什么能用动态代理?因为目标对象是 OnClickListener
接口的实例。
这里我先将代码贴上,咱们再解释。另外,强烈建议在看这一部分之前看完 代理模式与静态代理、动态代理的实现(Proxy.newProxyInstance、InvocationHandler)
这篇文章,否则再怎么解释也很难理解的。
Object proxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{listenerType}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method proxyMethod, Object[] args) throws Throwable {return method.invoke(context, args);}}
);setListenerMethod.invoke(view, proxy);
创建动态代理的三个参数,第一个选择当前 Activity
的 ClassLoader
;第二个是要设置的对应的接口,@OnClick
是 OnClickListener
接口,@OnLongClick
是 OnLongClickListener
接口;重点是第三个 InvocationHandler
。
当需要我们真的点击这个按钮的时候,点击事件会触发到 InvocationHandler.invoke
的方法,在这个方法中,我们必须通过反射调用被 @OnClick
或是 @OnLongClick
标记的函数,也就是在第一遍遍历中找到的那个 method
。
这个 Proxy.newProxyInstance
返回的是一个 Object
对象,但由于我们传入的是 listenerType
这个接口,因此其实际返回的就是一个 OnClickListener
或是 OnLongClickListener
接口的对象。而这个返回的对象,就是我们要设置到 setListenerMethod.invoke
中的参数。
至此,大功告成。上面是三个代码片段,下面我把 inject
整个方法的代码全部贴出来,大家看看。
public static void injectClick(Object context) {Class<?> clazz = context.getClass();Method[] methods = clazz.getDeclaredMethods();for(Method method: methods) {Annotation[] annotations = method.getAnnotations();for(Annotation annotation : annotations) {Class<? extends Annotation> annotationClass = annotation.annotationType();EventBase eventBase = annotationClass.getAnnotation(EventBase.class);if(eventBase == null)continue;try {Method valueMethod = annotationClass.getMethod("value");int[] viewIds = (int[]) valueMethod.invoke(annotation);if(viewIds == null)continue;for(int viewId: viewIds) {Method findViewByIdMethod = clazz.getMethod("findViewById", int.class);View view = (View) findViewByIdMethod.invoke(context, viewId);if(view == null)continue;String listenerSetter = eventBase.listenerSetter();Class<?> listenerType = eventBase.listenerType();Object proxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{listenerType},(proxy1, method1, args) -> method.invoke(context, args));Method setListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);setListenerMethod.invoke(view, proxy);}} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {exception.printStackTrace(System.err);}}}
}
总结
通过自定义 @OnClick
和 @OnLongClick
注解以及使用反射和动态代理技术,我们成功地简化了点击事件的处理代码。这样的实现不仅提高了代码的可读性和维护性,还展示了高级编程技术在实际项目中的应用。
虽然这种技术看起来很牛逼,但是里面使用了大量的反射,会慢一些。而且这种代码交接别人维护的话,肯定是一脸懵逼。所以我总结一下:
不如 setOnClickListener
,也就自己装逼用。
相关文章:

制作自己的 @OnClick、@OnLongClick(告别 setOnClickListener,使用注解、反射和动态代理)
前言 前面我们说过 ButterKnife 这个库,这个库实现不仅实现了 View 的绑定,而且还提供了大量的注解如 BindView、OnClick、OnLongClick 等来简化开发过程中事件绑定。而这些功能的实现是通过 APT 也就是注解处理器,在编译期间生成 Java 代码…...

Android基础-RecyclerView的优点
一、引言 在Android开发中,RecyclerView是一个强大而灵活的控件,用于展示大量数据集合的视图。相比于传统的ListView和GridView,RecyclerView提供了更高的性能、更多的布局选择和更丰富的交互体验。本文将详细阐述RecyclerView的功能作用以及…...

C# Winform 用户控件,扩展控件,自定义控件综合实例
Control类是Windows窗体控件的基类,它提供了在 Windows 窗体应用程序中进行可视显示所需的基础结构,可以通过继承来扩展熟悉的用户控件和现有控件的功能。本列介绍三种不同自定义控件以及怎么创建他们。 自定义控件分类 用户控件:基本控件的…...

经济学和金融学有什么区别?
中文版 金融学和经济学是两个密切相关但有所不同的学科,它们各自侧重于不同的研究领域和方法。 经济学 (Economics) 经济学是研究如何配置有限资源以满足人类无限需求的学科。它可以分为两个主要分支: 宏观经济学 (Macroeconomics): 研究经济整体的行…...

Java课程设计:基于swing的学生信息管理系统
文章目录 一、项目介绍二、项目展示三、源码展示四、源码获取 一、项目介绍 这款Java swing实现的学生信息管理系统和jsp版本的功能很相似,简单的实现了班级信息的增删改查,学生信息的增删改查,数据库采用的是mysql,jdk版本不限&…...

【代码】3d->video
mesh->video import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from matplotlib.animation import FuncAnimation import matplotlib.animation as animation from pywavefront import Wavefront import osnamedog # 读取OBJ文…...

算法01 递推算法及相关问题详解【C++实现】
目录 递推的概念 训练:斐波那契数列 解析 参考代码 训练:上台阶 参考代码 训练:信封 解析 参考代码 递推的概念 递推是一种处理问题的重要方法。 递推通过对问题的分析,找到问题相邻项之间的关系(递推式&a…...

自动化测试火狐下载文件
本篇文章介绍selenium中火狐浏览器如何下载文件。比如我想把这个MP4的视频文件下载下来。 点击之后查看下载的类型是video/mp4 指定使用火狐浏览器 profile webdriver.FirefoxOptions() # 设置firefox默认的下载路径,0表示桌面,1表示我的下载…...

基于JSP技术的定西扶贫惠农推介系统
开头语:你好呀,我是计算机学长猫哥!如果有相关需求,文末可以找到我的联系方式。 开发语言:JSP 数据库:MySQL 技术:B/S架构、JSP技术 工具:Eclipse、MySQL、Tomcat 系统展示 首…...

Linux 终端窗口设置为透明
Linux 终端窗口设置为透明 打开终端 右键鼠标 选择Profile Preferences 点击Background 选择 Transparent background 拖动滑条调整透明度 完成。...

MySQL 中 Varchar(50) 和 varchar(500) 区别是什么?
一. 问题描述 我们在设计表结构的时候,设计规范里面有一条如下规则: 对于可变长度的字段,在满足条件的前提下,尽可能使用较短的变长字段长度。 为什么这么规定?我在网上查了一下,主要基于两个方面 基于存储空间的考…...

强化RAG:微调Embedding还是LLM?
为什么我们需要微调? 微调有利于提高模型的效率和有效性。它可以减少训练时间和成本,因为它不需要从头开始。此外,微调可以通过利用预训练模型的功能和知识来提高性能和准确性。它还提供对原本无法访问的任务和领域的访问,因为它…...

提取 Excel单元格文本下的超链接
在Excel中,可以使用内置的函数来提取单元格中的超链接地址。如果你有一个包含超链接的单元格,例如B1,你可以使用以下步骤来提取这个超链接: 在一个新的单元格(例如C1)中,输入以下公式ÿ…...

一键安全体检!亚信安全携手鼎捷软件推出企业安全体检活动 正式上线
亚信安全联合鼎捷软件股份有限公司(以下简称“鼎捷软件”)正式推出“一键安全体检”服务。亚信安全网络安全专家将携手鼎捷软件数据安全专家,围绕企业的数智安全状况,进行问题探索与治愈、新问题预测与预警,在全面筛查…...

numpy - array(1)
一维数据:向量 二位数据:矩阵 维度超过三维的数据:张量 这些数据在numpy中统称array (1)使用穷举法创建多为数据,接受列表或者元组类型的数据 a numpy.array([1, 2, 3]) b numpy.array([[1, 2, 3], (4, 5, 6), [7, 8, 9]]) (2)创建所有元…...

师彼长技以助己(6)递归思维
师彼长技以助己(6)递归思维 递归思维-小游戏 思维小游戏 思维 小游戏:1 玩一个从1或2开始往上加的游戏,谁加到20就赢 如何保证一定赢呢?我们倒推,要先到20的话,谁先到17就赢,如此…...

Kali Linux 2024.2
Kali Linux 2024.2 版本(t64、GNOME 46 和社区包) 比平常晚了一点,但 Kali 2024.2 来了!延迟是由于实现这一目标的幕后变化所致,这也是人们关注的焦点。社区提供了大量帮助,这次他们不仅添加了新的软件包&…...

【Spine学习08】之短飘,人物头发动效制作思路
上一节说完了跑步的, 这节说头发发型。 基础过程总结: 1.创建骨骼(头发需要在上方加一个总骨骼) 2.创建网格(并绑定黄线) 3.绑定权重(发根位置的顶点赋予更多总骨骼的权重) 4.切换到…...

chatgpt的命令词
人不走空 🌈个人主页:人不走空 💖系列专栏:算法专题 ⏰诗词歌赋:斯是陋室,惟吾德馨 目录 🌈个人主页:人不走空 💖系列专栏:算法专题 ⏰诗词歌…...

用python把docx批量转为pdf
为保证转换质量,本文的方法是通过脚本和com技术调用office自带的程序进行转换的,因此需要电脑已经装有office。如果希望不装office也能用,则需要研究OpenXML技术,后面实在闲的慌(退休)再搞。 安装所需库 …...

项目采购管理
目录 1.概述 2.三个子过程 2.1.规划采购管理 2.2.实施采购 2.3.控制采购 2.4.归属过程组 3.应用场景 3.1.十个应用场景 3.2.软件开发项目 3.2.1. 需求识别和分析 3.2.2. 制定采购计划 3.2.3. 发布采购请求 3.2.4. 供应商评估与选择 3.2.5. 合同签订 3.2.6. 采购…...

Elasticsearch 认证模拟题 - 18
一、题目 为一个索引,按要求设置以下 dynamic Mapping 一切 text 类型的字段,类型全部映射成 keyword一切以 int_ 开头命名的字段,类型都设置成 integer 1.1 考点 字段的动态映射 1.2 答案 # 创建索引和索引模板 PUT my_index {"m…...

Python基础-速记笔记
Python的基础数据类型都有哪些? 1、字符串(string)2、布尔类型(bool)3、整数(int) 4、浮点数(float)5、列表(list)6、集合(set)7、元组(tuple)8、字典(dict) 其中不可变类型有: 字符串(string)、布尔类型(bool)、整数(int) 、浮点数(float)、元组(tup…...

青少年编程与数学 01-001开始使用计算机 02课题、计算机操作系统3_3
青少年编程与数学 01-001开始使用计算机 02课题、计算机操作系统3_3 四、Linux操作系统安装(一) 准备工作(二)设置BIOS/UEFI(三) 安装Linux(四)磁盘分区(五)安…...

填表统计预约打卡表单系统(FastAdmin+ThinkPHP+UniApp)
填表统计预约打卡表单系统:一键搞定你的预约与打卡需求 填表统计预约打卡表单系统是一款基于FastAdminThinkPHPUniApp开发的一款集信息填表、预约报名,签到打卡、活动通知、报名投票、班级统计等功能的自定义表单统计小程序。 📝 一、引言…...

IO模型和多路转接
叠甲:以下文章主要是依靠我的实际编码学习中总结出来的经验之谈,求逻辑自洽,不能百分百保证正确,有错误、未定义、不合适的内容请尽情指出! 文章目录 1.IO 概要1.1.IO 低效原因1.2.IO 常见模型1.2.1.阻塞 IO1.2.2.非阻…...

如何完美解决升级 IntelliJ IDEA 最新版之后遇到 Git 记住密码功能失效的问题
🛠️ 如何完美解决升级 IntelliJ IDEA 最新版之后遇到 Git 记住密码功能失效的问题 摘要 在这篇文章中,我们将详细探讨如何解决在升级到 IntelliJ IDEA 最新版(2024.1.3 Ultimate Edition)后遇到的 Git 记住密码功能失效的问题。…...

SpringCloud微服务架构(eureka、nacos、ribbon、feign、gateway等组件的详细介绍和使用)
一、微服务演变 1、单体架构(Monolithic Architecture) 是一种传统的软件架构模式,应用程序的所有功能和组件都集中在一个单一的应用中。 在单体架构中,应用程序通常由一个大型的、单一的代码库组成,其中包含了所有…...

flinksql BUG : flink hologres-cdc source FINISHED
org.apache.flink.runtime.JobException: The failure is not recoverable or the failure does not allow to restart.at org.apache.flink.runtime.executiongraph.failover.flip1.ExecutionFailureHandler...
现代密码学-国密算法
商用密码算法种类 商用密码算法 密码学概念、协议与算法之间的依赖关系 数字签名、证书-公钥密码、散列类算法 消息验证码-对称密码 ,散列类 安全目标与算法之间的关系 机密性--对称密码、公钥密码 完整性--散列类算法 可用性--散列类、公钥密码 真实性--公…...