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

主要是通过反射、注解、动态代理相关的知识,实现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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容