通过反射或代理通过运行时注解处理多个view的点击事件

正文

反射的性能较低。不建议这么使用哈。这么写主要是表述可以这么设计。基于这种设计,我们可以通过编译时技术去通过注解信息去改变对应的class 生成对应的类。 我们知道APT技术可以生成java 文件。如果我们不想使用编译时技术呢?

回归到正题,在开发过程中,我们经常对于view设置点击事件,那么我们就需要写很多onClick 事件。我们知道反射的时候可以获取到对应的函数,那么通过反射+注解如何去实现这个功能呢? 我们将需求拆分下:

  • 一个类中有很多的函数,所以我们需要一个函数上的标记,标记这个函数需要通过反射调用。
  • 估计有很多view调用同一个点击事件,所以标记里面需要存id.因为id 是常量。view的变量。
  • 通过函数注解获取到的是id,不是view。而点击事件需要设置到view 上,所以我们需要通过反射获取到view,所以需要反射findviewByid()
  • 因为我们不想写onClick 事件。但是这个代码不能没有,所以得有人帮我们写。
  • 我们需要再写onClick 的地方 调用我们标记的函数。

第一步定义注解

反射是运行时的,所以注解的生命周期应该也是运行时,才能够获取到注解,我们需要获取到多个id,所以需要存储多个值。

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int [] ids() default {-1};
}

获取到函数

java 复制代码
public class InjetUtils {
    public static void injectClick(Object context){
        Class<?> aClass = context.getClass();
        Method[] methods = aClass.getDeclaredMethods();
        for (Method method: methods){
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (null==onClick){
                continue;
            }
            int[] ids = onClick.ids();
            try {
                Method findViewById = aClass.getMethod("findViewById", int.class);
                for (int id: ids){
                   View view=(View)findViewById.invoke(context,id);
                   view.setOnClickListener(new View.OnClickListener() {
                       @Override
                       public void onClick(View v) {
                           try {
                               method.invoke(context,v);
                           } catch (IllegalAccessException e) {
                               e.printStackTrace();
                           } catch (InvocationTargetException e) {
                               e.printStackTrace();
                           }
                       }
                   });
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

动态代理

动态代理特性,持有谁就是代理谁。那么我们基于动态代理去做呢? 我们基于上面的诉求继续拆解。

  • 我们依旧需要在某个地方调用我们标记的函数。
  • 所以我们动态代理生成的class 只能说点击事件的class.
  • 我们还是需要将动态代理生成的对象设置到view 上。

定义 InvocationHandler

我们知道,动态代理对象的函数都会执行到InvocationHandler的invoke里面。基于上面的诉求,那么我们就需要再invoke里面调用我们被标记的函数。

java 复制代码
public class ListenerInvocationHandler implements InvocationHandler {
    // 这个是我们的activity
    private Object object;
    // 这个是我们注解标记的对象函数
    private Method activityMethod;

    public ListenerInvocationHandler(Object object, Method activityMethod) {
        this.object = object;
        this.activityMethod = activityMethod;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.e("111111111", "invoke: "+method.getName() );
        return activityMethod.invoke(object,args);
    }
}

所以我们将对象和函数传递进来。因为我们Demo的函数的入参和onOnclick 入参一样。所以可以直接这么调用。

生成 OnClickListener的代理对象并且设置上去

java 复制代码
Object proxy = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new ListenerInvocationHandler(context, method));
                    view.setOnClickListener((View.OnClickListener) proxy);

和反射进行对比

我们只是将

java 复制代码
view.setOnClickListener(new View.OnClickListener() {
                       @Override
                       public void onClick(View v) {
                           try {
                               method.invoke(context,v);
                           } catch (IllegalAccessException e) {
                               e.printStackTrace();
                           } catch (InvocationTargetException e) {
                               e.printStackTrace();
                           }
                       }
                   });

替换成了:

java 复制代码
Object proxy = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new ListenerInvocationHandler(context, method));
                    view.setOnClickListener((View.OnClickListener) proxy);

这么写好处就是看起来没有了OnClickListener,但是代码是有的。

使用

java 复制代码
  @OnClick(ids = {R.id.tvBtn1,R.id.tvBtn2,R.id.tvBtn3})
    public void demo(View view){
        Toast.makeText(this,"---------",Toast.LENGTH_SHORT).show();
    }

结束

因为是调用函数,所以说 demo函数是私有的,就会报错。这种可以功能抽离成一个class的,就可以用APT去生成class,而不是使用反射调用。比如说现在的viewbinding 就会基于xml去生成一个class文件。而我们这种思路却是需要在需要的代码里面主动调用一次:InjetUtils.injectClick(this),而且标记的函数也只能有一个。

相关推荐
儿歌八万首6 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19438 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs8 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&8 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
LDORntKQH9 小时前
基于深度强化学习的混合动力汽车能量管理策略 1.利用DQN算法控制电池和发动机发电机组的功率分配 2
android
冬奇Lab9 小时前
Android 15 ServiceManager与Binder服务注册深度解析
android·源码·源码阅读
2501_9160088910 小时前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
独行soc12 小时前
2026年渗透测试面试题总结-20(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
常利兵12 小时前
2026年,Android开发已死?不,它正迎来黄金时代!
android
Risehuxyc12 小时前
备份三个PHP程序
android·开发语言·php