Android用注解与反射实现Butterknife功能

作者:发飙的蜗牛YR 时间:2022-02-18 17:27:17 

自定义注解

1) 先定义布局文件注入

//注解的作用域在类上
@Target(ElementType.TYPE)
//让保持性策略为运行时态,将注解编码到class文件中,让虚拟机读取
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
   int value();//使用时直接@ContentView(R.layout.xxx)指定的R.layout.xxx就是布局文件,会自动注入布局文件
}

2) 布局中控件注入文件

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
   int value();
}

3) 控件的点击响应注入文件

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface OnClick {
   int[] value();
}

使用自定义注解

@ContentView(R.layout.activity_test)
public class TestActivity extends AppCompatActivity implements View.OnClickListener{
   @ViewInject(R.id.edit_text)
   EditText mEditText;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       injectContentView(this);
       injectView(this);
       injectEvent(this);
   }
   @OnClick(R.id.button)
   @Override
   public void onClick(View v) {
       Toast.makeText(TestActivity.this,"点击成功"+mEditText.getText().toString(),Toast.LENGTH_SHORT).show();
   }

通过反射机制获取注解参数

Method相关方法的介绍:

method.invoke(Object obj,Object args[])的作用就是调用method类代表的方法,其中obj是对象名,args是传入method方法的参数

  • getMethods(): 获得类的public类型的方法

  • getMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型

  • getDeclaredMethods(): 获取类中所有的方法(public、protected、default、private)

  • getDeclaredMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型

1. 布局文件获取

public static void injectContentView(Activity activity){
       //获取activity的类实例
       Class<? extends Activity> clazz = activity.getClass();
       //获取到activity的ContentView注解
       ContentView contentView = clazz.getAnnotation(ContentView.class);
       if(contentView!=null){
           //如果activity上面存在这个注解的话,就取出这个注解对应的value值,就是前面设置的布局
           int layoutId=contentView.value();
           try {
               //利用反射调用setContentView方法,完成注入
               Method setViewMethod = clazz.getMethod("setContentView", int.class);
               setViewMethod.invoke(activity,layoutId);
           } catch (Exception e) {
               e.printStackTrace();
           }
       }
   }

2. 控件获取实现

private static void injectView(Activity activity){
       //获取activity的类实例
       Class<? extends Activity> clazz = activity.getClass();
       //获取activity的所有成员变量
       Field[] fields = clazz.getDeclaredFields();
       //遍历成员变量,获取成员变量上的ViewInject注解
       for(Field field:fields){
           //获取字段上面的注解对象,同有则继续下一个字段
           ViewInject viewInject = field.getAnnotation(ViewInject.class);
           if(viewInject!=null){
               //获取ViewInject注解的View的id
               int viewId = viewInject.value();
               //获取控件
               View view = activity.findViewById(viewId);
               try {
                   //设置field为可访问,就算私有的也能访问到,能够提高效率
                   field.setAccessible(true);
                   //将该控件设置给field对象
                   field.set(activity,view);//将activity对象的view控件设置给属性对应上面的例子是mEditText=findViewById(viewId)
               }catch (IllegalAccessException e){
                   e.printStackTrace();
               }
           }
       }
   }

3. 控件点击响应

private static void injectEvent(final Activity activity) {
       Class<? extends Activity> clazz = activity.getClass();
       //获取所有方法(私有方法也可以获取到)
       Method[] methods = clazz.getDeclaredMethods();
       for (Method method : methods) {
           //获取方法上面的OnClick注解
           OnClick click = method.getAnnotation(OnClick.class);
           //有则继续下面代码
           if (click != null) {
               //获取注解中的数据,因可以给多个button绑定点击事件,因此定义的注解类时使用的是int[]数据类型
               int[] viewId = click.value();
               //在需要发射时将method的setAccessible设置为true,可以提高反射速度,原因是设置为true后则跳过了访问检查,即使private修饰的也可以访问。
               method.setAccessible(true);
               //设置一个代理对象,当调用setOnClickListener时,把代理对象传进去,当点击发生时,就会invoke方法,可以调用带有onClick注解的method方法
              //此时的listener就是actvivty实现View.OnClickListener接口,并实现其接口中onClick方法的对象,其实也就是返回的invoke对象。
               Object listener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
                       new Class[]{View.OnClickListener.class}, new InvocationHandler() {
                   //这个invoke,其实就是调用的意思,第一个参数就是被代理的对象,第二个参数就是调用的方法,第三个参数是被调用方法的参数数组。
                           @Override
                           public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                               Log.d(TAG, "method"+method);
                               Log.d(TAG, "args "+args);
                               //执行activity对象,参数args的方法,activity对象就是当前的Activity,并且已经实现View.OnClickListener
                               //也就是说如果点击了控件将会执行实现了View.OnClickListener的Activity的onClick方法
                               Object invoke = method.invoke(activity, args);
                               return invoke;
                           }
                       });
                //listener是一个OnClickListener类的对象,因为method传的是activity,所以是当前类的OnClickListener对象
               //method是onClick方法
               try {
                   for (int id : viewId) {
                       //获取相应的控件
                       View v = activity.findViewById(id);
                       //"setOnClickListener"是方法名,View.OnClickListener.class方法参数的类型
                       Method setClickListener = v.getClass().getMethod("setOnClickListener", View.OnClickListener.class);
                       Log.d(TAG, "injectEvent: "+listener+"v.getClass()"+v);
                       //View类中的setOnClickListener方法需要一个OnClickListener类型的参数也就是我们的代理对象
                       //我们需要将对应的控件进行设置
                       //这时当我们点击v(也就是我们找到的控件)时就会调用代理对象的onClick方法。而代理对象的onClick方法就是Activity中实现接口的onClick方法。相当于执行方法setOnClickListener(listener)。而这个listener就是我们上面代理(当前activity实现OnClicker接口的对象)对象。
                       setClickListener.invoke(v, listener);
                   }
               } catch (Exception e) {
                   e.printStackTrace();
               }
           }
       }
   }

上面的实现都是在运行时通过反射完成注入,但它会对性能有一定损耗,而Butterknife用的是APT(Annotation Processing Tool)编译时解析技术。它在Java代码编译成Java字节码时就已经处理了@Bind, @OnClick这些注解。它的原理是读入Java源代码,解析注解,生成新的Java代码,新生成的Java代码最后被编译成Java字节码。

来源:https://blog.csdn.net/ChenYiRan123456/article/details/125950932

标签:Android,Butterknife
0
投稿

猜你喜欢

  • 基于Spring Boot保护Web应用程序

    2022-11-15 19:14:48
  • springboot html调用js无效400问题及解决

    2023-06-24 02:11:54
  • Android应用开发中Fragment存储功能的基本用法

    2022-10-16 20:54:35
  • Android游戏开发学习②焰火绽放效果实现方法

    2023-10-26 08:54:53
  • C# 数独求解算法的实现

    2022-04-04 18:48:57
  • Android AsyncTask详解及使用方法

    2022-09-24 04:53:29
  • C#事件标准命名规则及说明(包括用作事件类型的委托命名)

    2022-02-27 06:57:43
  • Flutter 给列表增加下拉刷新和上滑加载更多功能

    2021-10-01 13:30:18
  • C#实现redis读写的方法

    2023-07-13 16:21:35
  • Spring Boot环境下Mybatis Plus的快速应用操作

    2023-04-08 13:27:33
  • 关于SpringBoot使用Redis空指针的问题(不能成功注入的问题)

    2023-09-04 01:30:03
  • JavaWeb搭建网上图书商城毕业设计

    2023-08-29 00:08:32
  • android实现简易计算器

    2023-06-21 04:26:09
  • Android开发中Activity之间切换出现短暂黑屏的解决方法

    2023-02-19 04:29:38
  • JAVA ArrayList详细介绍(示例)

    2023-02-18 22:30:56
  • IntelliJ IDEA使用教程从入门到上瘾(2019图文版)

    2023-03-30 17:00:49
  • sharding-jdbc5.0.0实现分表实践

    2023-12-07 10:12:26
  • c#利用Excel直接读取数据到DataGridView

    2023-02-22 13:39:07
  • Android自定义view实现圆形进度条效果

    2021-09-12 00:30:03
  • Android WebView使用的技巧与一些坑

    2022-10-18 12:57:23
  • asp之家 软件编程 m.aspxhome.com