-->

Android

2021-01-16 02:55发布

文章目录

  • Java注解产生背景
  • Java 注解是什么
  • 作用是什么
    • 编译时注解
    • 运行时注解
    • 二者的区别
  • 本质是什么
  • 注解的分类
    • 预定义注解
      • @Override
      • @Deprecated
      • @SuppressWarnings
      • @SafeVarargs
      • @FunctionInterface
    • 元注解
      • @Target
      • @Retention
      • @Documented
      • @Inherited
    • 自定义注解
  • 注解使用示例
    • 1. @NonNull
    • 2. 代替简单枚举
    • 3. Butterknife @BindView 实现
    • 4. AopArms 项目源码
  • 其他问题
    • 注释和注解的区别
    • 注解是如何解决反射的问题?
  • 总结
  • 参考

在看静态代理和动态代理的区别时了解到反射,对反射技术做了总结。

而之前看一些文章总是提到使用注解能解决反射导致的性能问题。
因此想通过一篇文章来了解注解。但还是太年轻,一篇文章还是写不完。目前就先记录当下我所了解的注解。

Java注解产生背景

Java1.5之前的开发中,配置文件只能是通过xml文件来配置
这种方式的优点是

  • 集中管理对象和对象之间的组合关系,易于阅读
  • 遵循开闭原则,修改配置文件即可进行功能扩展
  • 弱耦合

但是也有它的不足

  • 需要同时维护 xml 配置文件和 Java 代码文件,开发速度慢
  • xml 配置文件过多,会导致维护变得困难
  • xml 解析会影响到应用程序的性能
  • 编译时很难检查出错误
  • 运行中的错误很难定位,调试难度较大

对于上述的问题,Java1.5后提供了注解的方式给开发人员更多的选择余地
相对于 xml 配置文件,注解的优势是

  • 注解与代码在一起,只要维护 Java 代码即可,开发速度快
  • 编译期间容易发现错误的出处
  • 还可结合注解处理器生成字节码

当然注解也不是用来替代xml配置的文件。注解也存在一些不足

  • 修改的话比较麻烦。如果需要对注解进行修改的话,就需要对整个项目重新编译
  • 处理业务类之间的复杂关系,不像 xml 那样容易修改,也不及 xml 那样明了
  • 如果后来的人对注解不了解,会给维护带来成本
  • 高耦合

小结:
xml 配置文件适合做一些全局的、与代码无关的配置。如:全局的配置

注解适合做一些和代码联系紧密的操作。如:页面跳转,那么路径就是和代码紧密相关的,因此可以用注解来配置路径。

Java 注解是什么

Java 注解(Annotation)又称之为 Java 标注元数据,是 Java 1.5 之后加入的一种特殊语法。

元数据(metadata)是指 描述数据的数据。举例:一个 图片文件 可能会包括描述图片大小、色彩深度、图片分辨率、图片创建时间 等资料的元数据。

作用是什么

  • 为编译器提供信息:编译器可以使用注解来检查错误或抑制警告,增强代码可读性。如: @NonNull
  • 编译时处理:可以生成代码、XML、文件等。
  • 运行时处理:注解可以在运行时检查

我们知道,代码只是我们跟机器沟通的一种语言。
我们编写代码首先会经过编译器的编译,编译器觉得没有问题才继续执行,执行完了机器才会知道我们要表达的内容。
这个内容有可能是正确的,也可能是错误的,但是起码我们能和机器沟通了。

就像比划猜词游戏:
出题人就是编译器,比划词的人是写代码的人,而猜词的人就是机器。

编译时注解

编译时:编译器将源代码翻译成机器能识别的代码。比如说 当你语法写错时编译器能告诉你代码写错了。

未添加@Retention(RetentionPolicy.CLASS) 标识的就是编译时注解,默认的就是 RetentionPolicy.CLASS

运行时注解

运行时:通俗点说就是代码在机器上执行了。比如说Android开发中的空指针异常,写代码时编译器是不知道那块会出现空指针异常,只有在代码执行出现空指针异常 App 才会报错。

在你的代码中加上这一行,就是运行时注解。表示该注解可以保留到运行期间

@Retention(RetentionPolicy.RUNTIME)

比如:


@Retention(RetentionPolicy.RUNTIME)
@Entity(id=10) // 自定义注解,暂时不用管
public class User(){
   
   }

上面的代码在运行时可以通过反射获取到@Entity(id=10)中的数据

二者的区别

  • 编译时注解作用范围仅在编译阶段,在代码运行阶段注解数据会被擦除。而运行时注解在代码运行阶段注解数据不会被擦除
  • 编译时注解的本质是生成辅助文件提供给程序运行时使用,运行时注解的本质是反射
  • 编译时注解会增加编译时间,运行时注解会使程序执行更耗时

本质是什么

The common interface extended by all annotation types

所有的注解类型都继承自这个普通的接口(Annotation)。

所以注解的本质就是一个继承了 Annotation 接口的接口

如:@Override 注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
   
   

}

本质上是继承了Annotation接口的接口:

public interface Override extends Annotation{
   
   
    
}

注解的分类

预定义注解

Java 或者 Android 中提供的注解。
下面仅罗列几个看看。

@Override

说明方法是子类重写父类的方法。(用于编译时检查)。

@Deprecated

说明方法已经过时,不建议使用,同时需要给出建议使用的方法。

@SuppressWarnings

忽略警告。

@SafeVarargs

忽略调用参数为泛型的方法的警告。

@FunctionInterface

标注一个接口是函数式接口。(只有一个方法的接口)

元注解

在注解上面的注解称为元注解(meta-annotations),如:

@Target({
   
   ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface UserField {
   
       
    int age();    
    String name();
}

@Target

用来描述注解的使用范围,也就是用来约束自定义注解可以注解Java的哪些元素。

也就是该注解要使用到哪个位置,具体由枚举 ElementType 中定义,具体如下:

public enum ElementType {
   
   
    TYPE,           //类、接口、注解、枚举
    FIELD,          //属性(包括枚举常量) 
    METHOD,         //方法
    PARAMETER,      //参数 
    CONSTRUCTOR,    //构造方法
    LOCAL_VARIABLE, //局部变量
    ANNOTATION_TYPE,//注解, 也就是元注解
    PACKAGE,        //包
}


@Retention

指定注解的存储方式,表明标记的注解可以多次应用于同一声明或类型使用。

我们由 RetentionPolicy.java (是一个枚举)可知,如:

public enum RetentionPolicy {
   
       
	SOURCE, // 标记的注解仅保留在源级别中,并被编译器忽略。    
	CLASS, // 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。    
	RUNTIME // 标记的注解由 JVM 保留,因此运行时环境可以使用它。
}

@Documented

表示使用了指定的注解,将使用 Javadoc 工具记录这些元素。

比如我们用@Document注解了我们的自定义注解:

import java.lang.annotation.Documented;

@Documented
public @interface MyAnnotation {
   
   

}

如果一个类使用了这个注解:

@MyAnnotation
public class MySuperClass {
   
    ... }

那么当生成MySuperClass的JavaDoc的时候,@MyAnnotation也会出现在JavaDoc当中。

@Inherited

表示注解类型可以从父类继承。

比如有一个自定义注解:

java.lang.annotation.Inherited

@Inherited
public @interface MyAnnotation {
   
   

}

如果有一个类使用了上面这个注解:

@MyAnnotation
public class MySuperClass {
   
    ... }

那么这个类的子类也会继承这个注解:

因为MySubClass 继承了MyClass,而MyClass的注解@MyAnnotation是可继承的,最终MySubClass也会有@MyAnnotation注解。

public class MySubClass extends MySuperClass {
   
    ... }

自定义注解

注解的定义类似于接口的定义,在关键字 interface 前加上 @

如:

public @interface UserField {
   
       
	int age();    
	String name();
}

int age()String name() 是注解类型(annotation type),它们也可以定义可选的默认值,如:

public @interface UserField {
   
       
	int age();    
	String name() default "张三";
}

在使用注解时,如果定义的注解的注解类型没有默认值,则必须进行赋值,如:

// 需声明运行时保留,不然获取不到数据
@Retention(RetentionPolicy.RUNTIME)
public @interface UserField {
   
   
    int age();
    String name() default "张三";
}

@UserField(age = 23,name = "李四")
class UserBean {
   
   

}

val usrField = UserBean::class.java.getAnnotation(UserField::class.java)
val age = usrField?.age
val name = usrField?.name
Log.e(TAG,"usrField 注解age:$age")
Log.e(TAG,"usrField 注解name:$name")

输出:
usrField 注解age:23
usrField 注解name:李四

注解使用示例

Android 开发中使用的第三方库就大量使用了注解,如:

运行时注解:Retrofit
编译时注解:Dagger2, ButterKnife, EventBus3

1. @NonNull

作用:用来标识特定的参数或者返回值不可以为 null

实现:

fun getMsg(@NonNull msg:String) {
   
   
    // msg=null 时会报 NullPointerException
    msg.toString(); 
}

// 编译时 提示参数不能为 null
// Null can not be a value of a non-null type String
getMsg(null);

2. 代替简单枚举

枚举类比常量更占内存, 因为一个Java对象至少占16个字节, 而枚举类包含了3个Java对象;
因此可以考虑用注解代替简单枚举

作用:使用注解实现语法检查。

实现:

// 枚举
object WeekDayDemo{
   
   

    private lateinit var weekDay: WeekDay

    enum class WeekDay{
   
   
        SATURDAY,SUNDAY
    }

    fun getWeekDay():WeekDay{
   
   
        return weekDay
    }

    fun setWeekDay(weekDay: WeekDay){
   
   
        this.weekDay = weekDay
    }
}
// 调用
WeekDayDemo.setWeekDay(WeekDayDemo.WeekDay.SATURDAY)
WeekDayDemo.getWeekDay()

// 使用注解,现在参数
public class WeekDayAnnotationDemo {
   
   
    public static final String SATURDAY = "SATURDAY";
    public static final String SUNDAY = "SUNDAY";

    private static String weekDay;


    @StringDef({
   
   SATURDAY, SUNDAY})
    @Target({
   
   ElementType.FIELD,ElementType.PARAMETER})
    @Retention(RetentionPolicy.SOURCE)
    @interface WeekDay{
   
   
        //自定义一个 WeekDay 注解
    }

    public static void setWeekDay(@WeekDay String weekDay) {
   
   
        WeekDayAnnotationDemo.weekDay = weekDay;
    }

    public static String getWeekDay() {
   
   
        return weekDay;
    }
}

WeekDayAnnotationDemo.setWeekDay(WeekDayAnnotationDemo.SATURDAY)
WeekDayAnnotationDemo.getWeekDay()

3. Butterknife @BindView 实现

作用:替代 findViewById

如果使用 kotlin 开发, 推荐使用方式一。

  • 方式一:使用kotlin插件自动生成
引入kotlin扩展插件

apply plugin: 'kotlin-android-extensions'

引入kotlin自动生成的相关布局文件

import kotlinx.android.synthetic.main.activity_main.*


在UI中使用
tv_hello_annotation.text = "hello annotation"
  • 方式二:使用注解
object InjectView {
   
   
    fun init(activity:Activity){
   
   
        try {
   
   
            val aClass = activity.javaClass
            val declaredFields = aClass.declaredFields
            for ( field in declaredFields){
   
   
                if (field.isAnnotationPresent(BindView::class.java)){
   
   
                    val bindView = field.getAnnotation(BindView::class.java)
                    val id = bindView?.value
                    if (id != null){
   
   
                        val view = activity.findViewById<View>(id)
                        field.isAccessible = true
                        field.set(activity,view)
                    }
                }
            }
        }catch (e:Exception){
   
   
            // exception
        }

    }
}

// 创建 @BindView 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
   
   
    @IdRes int value(); // 限制只能传 id 资源
}


// 在 activity 使用
class MainActivity : AppCompatActivity() {
   
   

    @BindView(R.id.tv_hello_annotation)
    private var tvData: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
   
   
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        InjectView.init(this)
        tvData?.text = "hello annotation"
    }
}

4. AopArms 项目源码

AopArms 项目基于沪江aspect插件库 同时利用注解的特性实现一些特定额业务场景。如:日志、拦截(登录)、异步处理、缓存、SP、延迟操作、定时任务、重试机制、try-catch安全机制、过滤频繁点击等。具体可点击下方链接查看。

源码:https://github.com/aicareles/AopArms

文章介绍:https://mp.weixin.qq.com/s/cjGADwzfb3hWtfYvm0J2LQ

其他问题

注释和注解的区别

注解用于机器去理解代码,是代码的一部分。注释用于人去理解代码,不是代码的一部分。

注解是如何解决反射的问题?

不知道你是否听说过:

使用注解可以解决反射的性能问题

这里说的注解大部分使用的是编译时注解,也有用运行时注解但是内部肯定做了缓存或者索引优化。

总结

  • 注解不是为了替代xml,而是提供另一种代码和配置文件之间联系的方式。
  • 注解的作用:
    • 在编译时给编译器提示
    • 在编译时可配合注解处理器生成辅助文件
    • 在运行时提供数据
  • 注解的本质是继承了annatation接口的接口
  • 注解的分类
    • 预定义注解:Java 常见的 @Override,@Deprecated,@SuppressWarmings,Android常见的 @NonNull
    • 元注解:@Target, @Retention, @Documented, @Inherited
    • 自定义注解:使用 @interface声明
      • 源码级注解:@Retention(RetentionPolicy.SOURCE)
      • 编译时注解:默认,使用@Retention(RetentionPolicy.CLASS)定义注解
      • 运行时注解:@Retention(RetentionPolicy.RUNTIME)
  • 注解是和代码有关是能提供给机器查看的,注释是代码无关的仅提供给人查看的
  • 大部分框架都用了注解去解决反射问题,这里的注解是结合注解处理器在编译阶段生成辅助代码并提供给运行时使用。这就导致编译时间会变长,但是由于没有使用反射,所以不会出现反射的性能问题。

参考

  • Android coder 需要理解的注解、反射和动态代理
  • Java | 这是一篇全面的注解使用攻略(含 Kotlin)
  • 教你实现一个轻量级的注解处理器(APT)
  • 轻松打造一个自己的注解框架
  • Java注解(Annotation)详解
  • Android中的注解原来也可以这么好玩!
  • AopArms一款基于AOP的Android注解框架
  • Android 如何编写基于编译时注解的项目
  • yangchong211/YCApt (自定义路由注解)

本文同步分享在 博客“_龙衣”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签: