Butterknife之从反射到注解,再到实现简单的butterknife效果。了解一下!

butterknife是现在使用的较多的一个库了,它让我们的工作减少了相当一部分的量,今天就让我们一起来走近它 ,实现我们自己的一个简单的类似效果,让它不再显得那么神秘吧!

谈及butterknife的原理,粗略的说,它用到了反射和注解这两个方面的知识。那么我们想实现简单的butterknife效果,我们就得首先知道反射和注解是怎么回事,ok,接下来我们先来看看反射吧。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
上述就是对反射的理解,有了文字含义,我们再来写一写小案例具体看看吧:

比如我们这有个User类,想要获取到这个类中的成员变量

public class User {
    private String name;
    public int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name == null ? "" : name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void like(String type){
        Log.e("aaa","喜欢的东西是:----"+type);
    }
}

那么我们想要通过反射来获得该类中的属性和方法:该怎么做呢?
从上述文字中,我们可以看到,想要解剖一个类:
第一步:就是先要获取到每一个字节码文件对应的Class类型的对象.
这个对象有三种方式可以获取:

//第一种  类名.class
Class class1= User.class;
//第二种  通过forName(全类名)
Class clazz2 = Class.forName("com.example.yinl.simplebutterknife.User");
// 第三种 通过对象的getClass()
User user = new User("zhangsan", 10);
Class clazz = user.getClass();
 
第二步:通过获得的对象来拿到成员属性
 //这里注意  当属性为private的时候  需要使用getDeclaredField()
            //如果是public  则也可使用getField()
            Field name=clazz.getDeclaredField("name");
            Field age=clazz.getField("age");
            //private属性值 还需设置 允许暴力反射
            name.setAccessible(true);
            //获得属性值  从user对象中获取
            String uName= (String) name.get(user);
            int uAge=age.getInt(user);

            //修改属性值  修改的对象  修改值
            name.set(user,"yinll");
            age.set(user,12);

//获得类中的方法
        //一:获得所有方法
        Method[] methods=clazz.getMethods();
        for(Method method:methods){
           //打印一下方法  做个测试
            Log.e("aaa","方法-----"+method.getName());
        }

        //二:获得某一个方法  方法名  方法类型
            Method method=clazz.getMethod("like",String.class);
            //调用该方法(在user对象中调用该方法,传入参数)
            method.invoke(user, "水果茶");

通过以上几个步骤,我们就能成功的获取到我们想要知道的类中的属性和方法,并对其进行修改和调用了。有木有很简单?

学会了反射之后,我们接下来看看 注解:
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。注解是以‘@注解名’在代码中存在的,根据注解参数的个数,我们可以将注解分为:标记注解、单值注解、完整注解三类。它们都不会直接影响到程序的语义,只是作为注解(标识)存在,我们可以通过[反射机制]编程实现对这些元数据(用来描述数据的数据)的访问。另外,你可以在编译时选择代码里的注解是否只存在于源代码级,或者它也能在class文件、或者运行时中出现(SOURCE/CLASS/RUNTIME)。

首先如何创建注解:定义一个类 将其设置为@interface。下方是我定义的一个注解:

  • 注解:我们需要注意的是,定义注解时我们需要给其设置 @Retention
    和 @Target , 具体使用情况详细见下方代码
//表示当前注解存在于字节码中,当源码被编译成字节码时,注解不会被清除
//在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
//@Retention(RetentionPolicy.CLASS)
//表示当前注解存在于源码中,当源码别编译成字节码时,注解会被清除
//在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码
//@Retention(RetentionPolicy.SOURCE)
//表示当前注解使用在方法上
//@Target(ElementType.METHOD)
//用于描述类、接口或enum声明
//@Target(ElementType.TYPE)

//表示当前注解存在于虚拟机
//始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
@Retention(RetentionPolicy.RUNTIME)
//表示当前注解使用在属性上
@Target(ElementType.FIELD)
public @interface BindView {

    // 如果注解中只有一个属性,可以直接命名为“value”,使用时无需再标明属性名。
    //@BindView(R.id.y)
//    int value();
    //如果是其他值 比如
    int age();
    String name();
    //则在使用时 需要表明属性名 如 @BindView(age=18,name="yinl")
}

定义好注解之后 我们就可以在类中来使用了:比如我们新建一个Test类,看看如何在这个类中来使用我们的注解

public class Test {

    /**
     * 使用注解  age和name在注解中有定义
     * 如果注解中只有一个属性,可以直接命名为“value”  
     * 此时 使用@BindView时不需要像下方一样知名age或者name属性
     * 直接@BindView(20)即可,@BindView(R.id.xx)就是这样,熟悉吧
     */
   
    @BindView(age=18,name="yinl")
    private String name ;
    private int age ;

    @Override
    public String toString(){
        return "name:"+name+",age:"+age;
    }
}

那到了这里,我们又如何去获取注解上的值呢?这时候我们上面学到的反射就有用啦!这里我们再新建一个类,就取名叫做Butterknife吧,我们来模仿一下:在类中我们也来一个bind方法,我们将我们需要解剖的类传入到这个类中进行解析,获取到我们想要的数据(中间除了上述说道的反射只是外,需要注意的就是我们怎么样来拿到注解?)

public class Butterknife {
   //这里传入需要解析的对象
    public static void bind(Test test) throws NoSuchFieldException, 
    IllegalAccessException {

        //获得字节码对象
        Class c= test.getClass();
        //获得属性
        Field name=c.getDeclaredField("name");
        Field age=c.getDeclaredField("age");
        //允许暴力反射
        name.setAccessible(true);
        age.setAccessible(true);

        /**
         * 获取注解  这里对应的是我们自定义的注解类,一定不要搞错了  一一对应的哦
         *  比如我们这里用到的是上方定义的BindView 注解,那我们需要获取到的注解对象 
          * 就是BindView ,getAnnotation(BindView.class)中传入的参数自然也就是 
          * BindView 
         */
        BindView bindView=name.getAnnotation(BindView.class);
        if(bindView != null){
          //如果注解不是空 则可以获取注解值
            String uName=bindView.name();
            int uAge=bindView.age();

          //然后将值设置到我们的对象上
            name.set(test,uName);
            age.set(test,uAge);
        }else{
            //注解为空
        }
    }
}

//接下来我们在activity中来调用看看:

//先定义对象
 Test t=new Test();
        try {
            //我们的Butterknife用起来  有木有很顺手,很hi。写起来都是一个感觉
            Butterknife.bind(t);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        //这时候我们可以来看看注解上的值是否成功的传递到了对象中
        Toast.makeText(this,t.toString(),Toast.LENGTH_SHORT).show();

上方的结果经测试,是ok的,那么到这里位置,反射和注解我们就都了解啦,简单中藏着奥妙,舒服。

接下来就是重头戏啦,自己实现一个简单的类似于Butterknife的功能,废话不多说,动起来:
首先我们实现通过@BindView(R.id.xx),实现获取控件的实例,并得到控件的值(省去findviewbyid)
第一步:先定义BindView注解类

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    // 如果注解中只有一个属性,可以直接命名为“value”,使用时无需再标明属性名。
    //@BindView(R.id.y)
    int value();
}

//这里可以看看跟上方BindView类中的区别,加深理解

第二步:自定义Butterknife类,在类中来获取我们需要的值,这里与上方不同的一点就是我们传入的对象应该是activity.另外就是大家可以多注意这里面是如何来找到我们的控件,做到不需要我们在activity中findviewbyid的。

public class Butterknife {
    public static void bind(Activity activity) {
       try {
            bindView(activity);       
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 绑定视图view
     */
    private static void bindView(Activity activity) throws IllegalAccessException {
        //获得字节码
        Class clazz=activity.getClass();
        //获得activity中的所有变量
        Field[] fields=clazz.getDeclaredFields();
        //循环获取
        for(Field field:fields){
          //允许暴力反射
            field.setAccessible(true);
            //获取变量上加的注解
            BindView bindView=field.getAnnotation(BindView.class);
            if(bindView != null){
                //注解不为空的情况下  获取注解的值  这里实际上是找到当前控件对应的ID
                int id=bindView.value();
                //通过id来获取控件
                View view=activity.findViewById(id);
                //将控件的值赋值给变量
                field.set(activity,view);
            }else{
            }
        }
    }
}

//到了这里我们就可以在activity中调用了:

    Butterknife.bind(this);
   //输出下是否获得到了控件的值
   Toast.makeText(this,textView1.getText().toString(),Toast.LENGTH_SHORT).show();

那么这里也可以成功得到textView1这个控件的值,这样我们就成功了一 半,实现了这种简单的控件绑定效果,而不需要通过findviewbyid来获取控件实例了;

下面我们再来实现 butterknife绑定点击事件的效果(就是butterknife中的@OnClick(R.id.xx)):
首先同样的我们这里 新建一个针对点击事件的注解类

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

接下来我们就该思考怎么通过这个注解来设置它的点击事件:我们想想上方的思路,肯定也是通过反射来得到当前控件的id,然后通过在我们自定义的Butterknfe封装类中获得该id的控件实例,有了控件实例,这时候我们是不是就可以给这个获得的实例对象添加点击事件监听啦?

所以我们回到Butterknfe类中,加入一个bindClick方法(记得bind()方法中加入调用):绑定我们的监听事件:

/**
     * 点击事件
     */
    private static void bindClick(final Activity activity){
        Class<? extends Activity> c=activity.getClass();
        //获得方法
        Method[] methods=c.getDeclaredMethods();
        for(final Method method:methods){
            //允许暴力反射
            method.setAccessible(true);
            //获得注解
            MyClick view=  method.getAnnotation(MyClick.class);
            if(view != null){
                //获得控件id
                int id=view.value();
                View v=activity.findViewById(id);
                //点击事件
                v.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        try {

                            //调用方法  这里方法没带参数 所以不用传递
                            method.invoke(activity);

                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }else{
            }
        }
    }

这样就成功的设置了监听事件啦,我们看看activity中的使用:同样是跟我们熟知的Butterknife是同样的使用方式:

 /**
     * 自定义 点击事件注解
     */
    @MyClick(R.id.button)
    public void onClick(){
        Log.e("aaa","onClick");
        Toast.makeText(ButterknifeActivity.this,"点击了按 
                                     钮",Toast.LENGTH_SHORT).show();
    }

经测试一切正常,大功告成。

一路走下来是不是发现其实特别的简单,并不复杂(当然我们工作中所使用到的Butterknife库远远不止这么多的逻辑),开不开心?希望看到这,能够让你基本的懂得了反射,注解,以及结合这两者如何实现类似于butterknife的功能。感谢您的观看,有问题欢迎留言指教!

最后附上传送门,需要的朋友可以参考下:不过建议自己写一波,这样比较好;
源码传送门.

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,226评论 25 707
  • 前言## 最近一段时间在研究EventBus和Retrofit 的过程中,都遇到了注解这个概念。由于在学习Java...
    IAM四十二阅读 9,672评论 18 86
  • 篱落疏疏一径深,树头花落未成阴。 儿童急走追黄蝶,飞入菜花无处寻。
    诗沐阳阅读 492评论 0 0
  • 今天来苏州第二天。 今天琼杰给介绍了一个食品厂,做销售,刚起步的销售部,工资不高,但是觉得还算靠谱,约好的面前全部...
    大白比小白白阅读 265评论 0 0
  • 我在熬一锅汤。汤里放多了盐,盐融进了汤里,汤太咸了。是否要加一些水冲淡?冲淡了,汤的鲜味就没了。这是一个值得思考的...
    胡清隐阅读 448评论 1 2