理解Android中的注解与反射

前言##

最近一段时间在研究EventBus和Retrofit 的过程中,都遇到了注解这个概念。由于在学习Java的时候对这方面没有深入了解过,所以看起相关的代码来,总会有点不知其所以然,这里就注解和反射的使用做一下总结。

这里我们先从反射说起,了解了反射的意义及用法后,我们在看看注解的使用,最后叙述一下在Android开发中是怎样结合使用注解与反射。

反射##

Java反射(Reflection)定义###

Java反射机制是指在运行状态

对于任意一个类,都能知道这个类的所有属性和方法;
对于任何一个对象,都能够调用它的任何一个方法和属性;

这样动态获取新的以及动态调用对象方法的功能就叫做反射。

比如像下面:

           //获取类
        Class c = Class.forName("java.lang.String");
        // 获取所有的属性
        Field[] fields = c.getDeclaredFields();
        StringBuffer sb = new StringBuffer();
        sb.append(Modifier.toString(c.getModifiers()) + " class " + c.getSimpleName() + "{\n");
        // 遍历每一个属性
        for (Field field : fields) {
            sb.append("\t");// 空格
            sb.append(Modifier.toString(field.getModifiers()) + " ");// 获得属性的修饰符,例如public,static等等
            sb.append(field.getType().getSimpleName() + " ");// 属性的类型的名字
            sb.append(field.getName() + ";\n");// 属性的名字+回车
        }
        sb.append("}\n");
        System.out.println(sb);

就可以获得 String ,这个我们常用类的所有属性:

string_property

再比如:

       //获取类
        Class c = Class.forName("java.lang.String");
        // 获取所有的方法
        Method[] ms = c.getDeclaredMethods();
        //遍历输出所有方法
        for (Method method : ms) {
            //获取方法所有参数
            Parameter[] parameters = method.getParameters();
            String params = "";
            if (parameters.length > 0) {
                StringBuffer stringBuffer = new StringBuffer();
                for (Parameter parameter : parameters) {
                    stringBuffer.append(parameter.getType().getSimpleName() + " " + parameter.getName() + ",");
                }
                //去掉最后一个逗号
                params = stringBuffer.substring(0, stringBuffer.length() - 1);
            }
            System.err.println(Modifier.toString(method.getModifiers())
                    + " " + method.getReturnType().getSimpleName()
                    + " " + method.getName()
                    + " (" +params  + ")");
        }

可以获得String 类的所有方法(图片只截取了部分方法,实际有很多就不占篇幅了):

string_method

Java反射机制API###

主要的几个类####

Java中有关反射的类有以下这几个:

用途
java.lang.Class 编译后的class文件的对象
java.lang.reflect.Constructor 构造方法
java.lang.reflect.Field 类的成员变量(属性)
java.lang.reflect.Method 类的成员方法
java.lang.reflect.Modifier 判断方法类型
java.lang.annotation.Annotation 类的注解

具体实现####

为了方便描述,这里我们创建一个类 TestClass

public class TestClass {
    private String address;
    private String port;
    private int number;

   public void printInfo() {
        System.out.println("info is " + address + ":" + port);
    }        
    private void myMethod(int number,String sex) {

    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }


}

这个类很简单,包含三个成员变量address,port和number,以及它们各自的get,set方法。
两个自定义的方法printInfo()和myMethod()。

下面我们就看一下如何通过反射,获取这个TestClass的所有“信息”

  • 1.获取Class
    关于Class的获取有三种写法:
//获取类的三种方法:
Class c = Class.forName("java.lang.String");  //这里一定要用完整的包名
Class c1=String.class;
String str = new String();
Class c2=str.getClass();

这里获取的c,c1以及c2都是相等的。一般在反射中会用第一种写法。

  • 2.获取类的属性(成员变量)
Field[] fields = c.getDeclaredFields();

这里返回的是一个数组 ,包含所有的属性。获取到的每一个属性Filed,包含一系列的方法可以获取及修改他的内容。
如下所示:

// 遍历每一个属性
        for (Field field : fields) {
            sb.append("\t");// 空格
            sb.append(Modifier.toString(field.getModifiers()) + " ");// 获得属性的修饰符,例如public,static等等
            sb.append(field.getType().getSimpleName() + " ");// 属性的类型的名字
            sb.append(field.getName() + ";\n");// 属性的名字+回车
        }

这里我们可以得到TestClass的所有属性:

test_p
  • 3.获取类的方法
// 获取所有的方法
Method[] ms = c.getDeclaredMethods();

和属性类似,我们依然可以通过一系列的方法获取到方法的返回值类型,名称以及参数。下面的表格中总结了一些关键方法:

reflection

类似的获取到TestClass的所有方法:

test_method

这里可以看到,获取的TestClass的属性和方法同我们定义的是完全一致的。

这里我们顺便调用一下TestClass的printInfo方法:

new TestClass().printInfo();

用于所有属性没有做初始化,所以得到如下输出:

null

可以看到,利用反射我们可以很方便的去“反编译”一个class。那么我们用反射这么做的意义是什么呢?不要着急,下面我们先来了解一下注解

Java 注解(Annotation)##

什么是注解###

关于注解的定义网上有很多说法,就不再赘述。这里我们就说两点

Annotation(注解)就是Java提供了一种源程序中的元素关联任何信息或者任何元数据(metadata)的途径和方法。

Annotation是被动的元数据,永远不会有主动行为

既然是被动数据,对于那些已经存在的注解,比如Override,我们只能看看而已,并不知道它具体的工作机制是什么;所以想要理解注解,就直接从自定义注解开始。

自定义注解###

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
@Inherited
public @interface Bind {
    int value() default 1;
    boolean canBeNull() default false;
}

这就是自定义注解的形式,我们用@interface 表明这是一个注解,Annotation只有成员变量,没有方法。Annotation的成员变量在Annotation定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。比如上面的value和canBeNull。

元注解###

可以看到自定义注解里也会有注解存在,给自定义注解使用的注解就是元注解。

@Rentention Rentention####

@Rentention Rentention用来标记自定义注解的有效范围,他的取值有以下三种:

**RetentionPolicy.SOURCE: ** 只在源代码中保留 一般都是用来增加代码的理解性或者帮助代码检查之类的,比如我们的Override;

**RetentionPolicy.CLASS: ** 默认的选择,能把注解保留到编译后的字节码class文件中,仅仅到字节码文件中,运行时是无法得到的;

**RetentionPolicy.RUNTIME: **,注解不仅 能保留到class字节码文件中,还能在运行通过反射获取到,这也是我们最常用的。

@Target####

@Target指定Annotation用于修饰哪些程序元素。
@Target也包含一个名为”value“的成员变量,该value成员变量类型为ElementType[ ],ElementType为枚举类型,值有如下几个:

  • ElementType.TYPE:能修饰类、接口或枚举类型
  • ElementType.FIELD:能修饰成员变量
  • ElementType.METHOD:能修饰方法
  • ElementType.PARAMETER:能修饰参数
  • ElementType.CONSTRUCTOR:能修饰构造器
  • ElementType.LOCAL_VARIABLE:能修饰局部变量
  • ElementType.ANNOTATION_TYPE:能修饰注解
  • ElementType.PACKAGE:能修饰包

使用了@Documented的可以在javadoc中找到
使用了@Interited表示注解里的内容可以被子类继承,比如父类中某个成员使用了上述@From(value),From中的value能给子类使用到。

好了,关于注解就说这么多。

反射&注解的使用##

属性值使用注解###

下面我们首先自定义两个注解:BindPort 和 BindAddress

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindPort {
    String value() default "8080";
}

指定BindPort 可以保留到运行时,并且可以修饰成员变量,包含一个成员变量默认值为”8080“。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindAddress {
    String value() default "127.0.0.0";
}

这个和上面类似,只是默认值为"127.0.0.0"。

同时,我们修改之前的TestClass

public class TestClass {
    @BindAddress()
    String address;
    @BindPort()
    private String port;

    private int number;

    public void printInfo() {
        System.out.println("info is " + address + ":" + port);
    }
   
   ........


}

这里我们将原先的address 和 port 两个变量分别用这里定义的注解进行修饰,由于我们在定义注解时有默认值,所以这里的注解可以不写参数。

使用反射获取注解信息####

前面已经说了,Annotation是被动的元数据,永远不会有主动行为,所以我们需要通过使用反射,才能让我们的注解产生意义。

通过反射可以获取Class的所有属性和方法,因此获取注解信息也不在话下。我们看代码:

        //获取类
        Class c = Class.forName(className);
        //实例化一个TestClass对象
        TestClass tc= (TestClass) c.newInstance();

        // 获取所有的属性
        Field[] fields = c.getDeclaredFields();

        for (Field field : fields) {
            if(field.isAnnotationPresent(BindPort.class)){
                BindPort port = field.getAnnotation(BindPort.class);
                field.setAccessible(true);
                field.set(tc,port.value());
            }

            if (field.isAnnotationPresent(BindAddress.class)) {
                BindAddress address = field.getAnnotation(BindAddress.class);
                field.setAccessible(true);        
                field.set(tc,address.value());
            }

        }

        tc.printInfo();

我们运行程序得到如下输出:

output

我们对tc 对象并没有做任何的set及初始化工作,输出结果却依然不再是null了,这就是反射与注解的功劳。

上面代码的逻辑很简单:

首先遍历循环所有的属性,如果当前属性被指定的注解所修饰,那么就将当前属性的值修改为注解中成员变量的值。

上面的代码中,找到被BindPort修饰的属性,然后将BindPort中value的值赋给该属性。

这里setAccessible(true)的使用时因为,我们在声明port变量时,其类型为private,为了确保可以访问这个变量,防止程序出现异常。

理论上来说,这样做是不安全的,不符合面向对象的思想,这里只是为了说明注解和反射举例。

但是,你也会发现,反射给我们提供了一种在运行时改变对象的方法。

好了,下面我们继续修改TestClass

public class TestClass {
    @BindAddress("http://www.google.com.cn")
    String address;
    @BindPort("8888")
    private String port;

    private int number;

    public void printInfo() {
        System.out.println("info is " + address + ":" + port);
    }
    .......
}

我们为注解设定了参数,再次运行,相信你已经猜到结果了。

output1

这时候由于我们在给成员变量设定注解时,写了参数,反射时也取到了相应的值。

方法使用注解###

上面对于类属性(成员变量)设定注解,可能还不能让你感受到注解&反射的优势,我们再来看一下类的方法使用注解会怎样。

我们还是先定义一个注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BindGet {
    String value() default "";
}

有效范围至运行时,适用于方法。

再次修改TestClass 如下:

public class TestClass {
    @BindAddress("http://www.google.com.cn")
    String address;
    @BindPort("8888")
    private String port;

    private int number;

    @BindGet("mike")
    void getHttp(String param){
        String url="http://www.baidu.com/?username"+param;
        System.err.println("get------->"+url);
    }

    ...........
}

我们添加了一个名为getHttp的方法,而且这个方法由@BindGet注解。

然后看反射的使用:

    //获取类
        Class c = Class.forName(className);
        TestClass tc= (TestClass) c.newInstance();
    
    // 获取所有的方法
        Method[] ms = c.getDeclaredMethods();

        for (Method method : ms) {
           if(method.isAnnotationPresent(BindGet.class)){
               BindGet bindGet = method.getAnnotation(BindGet.class);
               String param=bindGet.value();
               method.invoke(tc, param);
           }
        }

这里的逻辑和对属性的解析相似,依旧是判断当前方法是否被指定的注解(BindGet)所修饰,
如果是的话,就使用注解中的参数作为当前方法的参数去调用他自己。

这样,我们在运行程序时,通过反射就回去主动调用getHttp方法,得到如下输出:

output2

这里我们就可以通过注解动态的实现username参数的修改,甚至getHttp方法整个http url地址的修改。
(假设我们这里的getHttp方法是做网络请求)

到这里,你应该已经明白了如何使用反射获取注解的信息,但你一定会困惑这么做有什么用呢?

”动态“”动态“”动态“

这就是使用注解和反射最大的意义,我们可以动态的访问对象。

说了这么多,下面我们看看,在Android开发中,我们遇到的注解和反射。

Android 中的注解&反射##

Butterknife###

如果你是一个Android开发者,相信在使用Butterknife插件之前,你一定写了无数次的findViewById。

然而,如果使用了Butterknife 插件,我们就可以很方便的完成findViewById的工作,甚至是setOnClickListener 的工作。

public class ButtferknifeDemoActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_buttferknife);
        ButterKnife.bind(this);
        textView.setText("I'm not null");

    }
}

上面的代码,应该不陌生。试想如果你的activity_bufferknife 布局文件中有很多控件时,这样做不知道可以省多少时间了

我们看一下BindView的注解定义:

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

这个注解用于修饰变量,有效范围也是限定到了CLASS(即编译阶段),并没有到运行时。
我们在Butterknife(8.4.0)的部分源码中可以看到:

 /** Simpler version of {@link View#findViewById(int)} which infers the target type. */
  @SuppressWarnings({ "unchecked", "UnusedDeclaration" }) // Checked by runtime cast. Public API.
  @CheckResult
  public static <T extends View> T findById(@NonNull View view, @IdRes int id) {
    return (T) view.findViewById(id);
  }

我们可以猜到的,编译时最终的实现必然是到这里,实现view.findViewById(id)。

严格来说,这里使用Butterknife举例说明反射和注解是不太妥当的,因为Butterknife 注解的有效范围限定到了CLASS(编译阶段),所以到了运行阶段,不会有注解相关的事情发生,应该是APT(Annotation Processing Tool),编译是动态处理的方法,和这里的注解还是有区别的。

在这里,注解和反射的结合,使我们可以避免做很多重复的工作。

Retrofit 2.0

第一次使用Retrofit的时候,完全被接口定义的方式搞蒙圈了,完全搞不懂啊。

public interface UserBasicService {

    @GET("users/{user}")
    Call<ResponseBody> getUsers(@Path("user") String uses);
}

为什么要这么写?参数是怎么传递的?@ 是什么意思?带着曾经的这些疑问,我们首先看看这里的两个注解。

这里使用了两个注解GET 和Path ,我们看一下:

GET

/** Make a GET request. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
  /**
   * A relative or absolute path, or full URL of the endpoint. This value is optional if the first
   * parameter of the method is annotated with {@link Url @Url}.
   * <p>
   * See {@linkplain retrofit2.Retrofit.Builder#baseUrl(HttpUrl) base URL} for details of how
   * this is resolved against a base URL to create the full endpoint URL.
   */
  String value() default "";
}

Path

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface Path {
  String value();

  /**
   * Specifies whether the argument value to the annotated method parameter is already URL encoded.
   */
  boolean encoded() default false;
}

这两个注解的生命周期都延续到了 RUNTIME,即运行时。GET用于方法,Path用于参数。这点和我们定义getUsers()方法是一致的。

关于Retrofit中反射和注解的使用,涉及到动态代理的相关概念,这里就不展开来说。总的思路就是通过注解中使用的参数,动态的生成Request然后由OKHttp去调用。这个以后会做深入分析,这里只是了解注解和反射用法。

好了,关于注解和反射的使用方法及意义就暂时总结到这里。

后话##

以合适的方式使用反射,会让我们写代码的方式更加灵活。反射使用不当,反而会适得其反,会对性能造成影响。

但是EventBus,Retrofit 的如此火爆,让我们有理由相信,对性能的影响也许没那么大,或者说面对现如今硬件配置堪比电脑的手机,这点影响也许可以忽略不计。

所以关于反射的使用还是仁者见仁智者见智吧。


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

推荐阅读更多精彩内容