Android基础相关

手撸ButterKnife,主要练习反射、注解和动态代理

2021-01-25  本文已影响0人  来lol里

主要是通过反射、注解、动态代理相关的知识,实现ButterKnife的部分功能,其实用到的方法在Xutils和Dagger中都有涉及,就是IOC的依赖注入,利用依赖关系注入的方式,实现对象之间的解耦。简单点来说就是findViewById劳资写腻味了,不想写了,想通过其他的方式让他自己生成出来,当然还可以使用ATP的方式,那个是编译期的注解,新版本ButterKnife也是这么搞的,Google力推的viewbinding也是如此,这个回头再说,先来看我们运行时的注解。
项目传到github上了 地址

1、生成setContentView方法

先来看使用

@ContentViewInject(R.layout.activity_inject)
public class InJectActivity extends AppCompatActivity {
InjectUtils.ject(this);
}

看下注解类ContentViewInject,作用在Activity类上的,所以需要TYPE。然后是运行时需要用到的注解,所以是RetentionPolicy.RUNTIME

//      1.CONSTRUCTOR:用于描述构造器
//    2.FIELD:用于描述域
//    3.LOCAL_VARIABLE:用于描述局部变量
//    4.METHOD:用于描述方法
//    5.PACKAGE:用于描述包
//    6.PARAMETER:用于描述参数
//    7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Target(ElementType.TYPE)

//      1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
//      2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
//      3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentViewInject {
    int value();
}

然后看我们这个统一的工具类

public class InjectUtils {
    public static void ject(Context context){
        injectLayout(context);// 获取setContentView 方法
    }

可以在BaseActivity里声明一下,这里做示范,就直接在当前Activity里用了。
然后看一下这个injectLayout方法,注释写的很清楚了,就是通过反射获取Activity的类,然后拿到类上的注解ContentViewInject,再通过反射setContentView方法实现。

    private static void injectLayout(Context context) {
        int id;//当前activity的layout布局id
        Class<?> clazz=context.getClass();//获取当前activity的类
        ContentViewInject contentViewInject=clazz.getAnnotation(ContentViewInject.class); //获取ContentViewInject注解信息
        id=contentViewInject.value();//将获取到的当前注解中的layout的id赋值
        try {
            Method setContentView=clazz.getMethod("setContentView",int.class); //通过反射,获取setContentView方法,有参数需要带着int类型
            setContentView.invoke(context,id); //执行setContentView方法 第一个被反射类,第二个具体方法参数

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


    }

2、生成布局相关view的方法

    @ViewInject(R.id.bt1)
    private Button bt1;
    @ViewInject(R.id.bt2)
    private Button bt2;

再来看下view的方法,也是先上注解类ViewInject,这次是作用在变量上,所以是ElementType.FIELD。

@Target(ElementType.FIELD)

//      1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
//      2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
//      3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
    int value();

}

然后统一方法里加入

public static void ject(Context context){
        injectLayout(context);// 获取setContentView 方法

        injectView(context); //  获取每个view
    }

我们看下这个injectView方法,还是先通过反射的方式,拿到当前的Activity类,遍历当前所有的字段,查看有无注解信息,如果能查到我们的ViewInject注解信息,然后反射findViewById这个方法,当前的这个findViewById(当前注解信息),然后交给我们的变量bt1,大概就是当前字段bt1=findViewById(当前注解信息)。

 private static void injectView(Context context) {
        Class<?> clazz=context.getClass();//获取当前activity的类
        Field[] fields=clazz.getDeclaredFields();//获取当前所有

        for (int i = 0; i < fields.length; i++) {//遍历当前所有的成员方法,查看有无注解信息,如果有取出来

            ViewInject viewInject=fields[i].getAnnotation(ViewInject.class); //获取ContentViewInject注解信息

            if(viewInject!=null){
                try {
                    Method findViewById=clazz.getMethod("findViewById",int.class); //通过反射,获取setContentView方法,有参数需要带着int类型
                    fields[i].setAccessible(true);
                    View view=(View)findViewById.invoke(context,viewInject.value()); //相当于 调用findViewById,把当前的id,转换成view
                    fields[i].setAccessible(true);//防止用户设置私有
                    fields[i].set(context,view); //通过反射使用当前字段,把我们findViewById粗来的view,交给fields[i],即我们自己定义的那个bt1,  private Button bt1;

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

            }

        }

    }

然后在Activity里跑一下,发现bt1和bt2是生效的

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtils.ject(this);
        bt1.setText("修改成功");
        bt2.setText("bt2也修改成功了");
    }

3、生成view的点击交互方法

这个就比较麻烦了,会涉及到动态代理了,而且注解也麻烦一些,首先这个点击事件的方法,我们不知道用户到底是如何命名的,而且我们希望这个方法能兼容click或者longClick等其他的交互,注解这块就需要用到一个注解多态。

分解一下事件监听回调一般是用到三部分 setXXXListenernew onXXXListener和回调callback方法,我们其实主要关注前边的两个方法。先看下父注解的方法,主要是

//      ANNOTATION_TYPE:用在注解上
@Target(ElementType.ANNOTATION_TYPE)

//      1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
//      2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
//      3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseClickInject {
    //setXXXListener
    String listenerSetter();
    //  onXXXListener new出来的点击事件
    Class<?> listnerType();
}

再看下onClick事件和onLongClick的注解,返回数组是为了能在一个方法上调用多个字段,R.id.bt1、R.id.bt2,主要是传入点击事件所需要的信息

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//  bt2.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//
//            }
//        });
//  onLongClick 同样修改下边是哪个参数即可
@BaseClickInject(listenerSetter = "setOnClickListener",
                listnerType = View.OnClickListener.class,
                callbackMethod = "onClick" )
public @interface ClickInject {
    int[] value() default -1;
}
----------
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

//  bt2.setOnLongClickListener(new View.OnLongClickListener() {
//@Override
//public boolean onLongClick(View v) {
//        return false;
//        }
//        });
@BaseClickInject(listenerSetter = "setOnLongClickListener",
                listnerType = View.OnLongClickListener.class,
                callbackMethod = "onLongClick" )
public @interface LongClickInject {
    int[] value() default -1;
}

在InjectUtils方法里继续加入一个方法

   public static void ject(Context context){
        injectLayout(context);// 获取setContentView 方法
        injectView(context); //  获取每个view
        injectClick(context); // 获取view点击交互事件
    }

重点看一下这个injectClick方法,反射和注解的方法和上边的差不多,这里注释写的也比较详细,主要是动态代理这个方法需要看一下。

    private static void injectClick(Context context) {
        //为了处理onClick 、onLongClick。。等等事件,需要一个统一方法

        Class<?> clazz=context.getClass();//获取当前activity的类

        //getMethods 方法取得所有public方法 包括继承的方法

        //getDeclaredMethods 取得所有自己声明的方法 包括 public protected default private

        Method []methods=clazz.getDeclaredMethods();

        //遍历当前类中方法,查出有加入注解的方法
        for (int i = 0; i <methods.length ; i++) {
            Method method=methods[i];
            //遍历当前这个方法的所有注解
            Annotation[] annotations= method.getAnnotations();
            for (Annotation annotation:annotations) {
                Class<?> clazzAnnotation=annotation.annotationType();
                //为什么不用ClickInject,因为可能还要LongClickInject等其他交互方法,所以要判断他们的父注解信息
                BaseClickInject baseClickInject=clazzAnnotation.getAnnotation(BaseClickInject.class);
                if(baseClickInject!=null){ //判断当前这个注解的方法,是不是我们要处理的
                    method.setAccessible(true);//防止有人自己定义的点击方法私有
                    String listenerSetter=baseClickInject.listenerSetter();
                    Class<?> baseClickInjectClazz=baseClickInject.listnerType();
                    try {
                        Method valueMethod=clazzAnnotation.getDeclaredMethod("value"); //获取注解上的value方法
                        int[] values= (int[]) valueMethod.invoke(annotation); //获取当前所有的button对象id值了,没有参数类型
                        for (int value:values) {//遍历当前所有的button对象,通过反射根据view的id值获取view
                           Method findViewById= context.getClass().getMethod("findViewById",int.class);

                           View view= (View) findViewById.invoke(context,value);//根据反射出的方法,拿到当前view 即button

                           if(view!=null){
                              // activity对应的是context    myClick或者myLongClick对应的是我们自己定义的method 通过代理去执行我们自己定义的点击方法

                               MyInvokationHandler myInvokationHandler=new MyInvokationHandler(context,method);

                               //代理类   new View.OnClickListener()对象

                               //    /**
                               //     * 动态代理 能代理实现相同接口的方法 所以他代理其实就是把OnClickListener这个具体实现类代理出来
                               //     */
                               //         public interface OnClickListener {
                               //        /**
                               //         * Called when a view has been clicked.
                               //         *
                               //         * @param v The view that was clicked.
                               //         */
                               //        void onClick(View v);
                               //    }

                               Object proxy= Proxy.newProxyInstance(baseClickInjectClazz.getClassLoader()
                                       ,new Class[] {baseClickInjectClazz}
                                       ,myInvokationHandler);
                               //执行  让proxy执行的onClick()
                               //参数1  setOnClickListener()
                               //参数2  new View.OnClickListener()对象
                               //   view.setOnClickListener(new View.OnClickListener())其实就是  反射view上setOnClickListener方法

                               Method onClick=view.getClass().getMethod(listenerSetter,baseClickInjectClazz);
                               onClick.invoke(view,proxy);
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

定义的MyInvokationHandler类

 */
public class MyInvokationHandler implements InvocationHandler {

    //需要在onClick中执行activity.click();
    private Object activity;
    private Method activityMethod;

    MyInvokationHandler(Object activity, Method activityMethod){
        this.activity = activity;
        this.activityMethod = activityMethod;
    }
    /**
     * 就表示onClick的执行
     * 程序执行onClick方法,就会转到这里来
     * 因为框架中不直接执行onClick
     * 所以在框架中必然有个地方让invoke和onClick关联上
     */

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在这里去调用被注解了的click();
        return activityMethod.invoke(activity,args);
    }
}
上一篇下一篇

猜你喜欢

热点阅读