13.Android之注解问题

2,252 阅读18分钟

目录介绍

  • 13.0.0.1 什么是注解?系统内置的标准注解有哪些?SuppressWarnings用过没?Android中提供了哪些与线程相关的注解?
  • 13.0.0.2 什么是apt?apt的难点和优势?什么是注解处理器?抽象处理器中四个方法有何作用?annotationProcessor和apt区别?
  • 13.0.0.3 注解是怎么分类的?自定义注解又是怎么分类的?运行期注解原理是什么?实际注解案例有哪些?
  • 13.0.0.4 在自定义注解中,Annotation里面的方法为何不能是private?Annotation里面的方法参数有哪些?
  • 13.0.0.5 @Inherited是什么意思?注解是不可以继承的,这是为什么?注解的继承这个概念该如何理解?
  • 13.0.0.6 什么是依赖注入?依赖注入案例举例说明,有哪些方式,具备什么优势?依赖查找和依赖注入有什么区别?
  • 13.0.0.7 路由框架为何需要依赖注入,不用的话行不行?路由用什么方式注入,这些注入方式各具何特点,为何选择注解注入?
  • 13.0.0.8 实际开发中使用到注解有哪些,使用注解替代枚举?如何通过注解限定传入的类型?为何说枚举损耗性能?

注解基础系列博客

  • 01.Annotation注解详细介绍
    1.Annotation库的简单介绍
    2.@Nullable和@NonNull
    3.资源类型注释
    4.类型定义注释
    5.线程注释
    6.RGB颜色纸注释
    7.值范围注释
    8.权限注释
    9.重写函数注释
    10.返回值注释
    11.@Keep注释
    12.@SuppressWarnings注解
    13.其他
    
  • 02.Dagger2深入分析,待更新
  • 03.注解详细介绍
    • 什么是注解,注解分类有哪些?自定义注解分类?运行注解案例展示分析,以一个最简单的案例理解注解……使用注解替代枚举,使用注解限定类型
  • 04.APT技术详解
    • 什么是apt?理解注解处理器的作用和用途……android-apt被替代?annotationProcessor和apt区别? 什么是jack编译方式?
  • 06.自定义annotation注解
    • @Retention的作用?@Target(ElementType.TYPE)的解释,@Inherited注解可以被继承吗?Annotation里面的方法为何不能是private?
  • 07.注解之兼容kotlin
    • 后期更新
  • 08.注解之处理器类Processor
    • 处理器类Processor介绍,重要方法,Element的作用,修饰方法的注解和ExecutableElement,了解修饰属性、类成员的注解和VariableElement……
  • 10.注解遇到问题和解决方案
    • 无法引入javax包下的类库,成功运行一次,修改代码后再运行就报错
  • 11.注解代替枚举
    • 在做内存优化时,推荐使用注解代替枚举,因为枚举占用的内存更高,如何说明枚举占用内存高呢?这是为什么呢?
  • 12.注解练习案例开源代码
    • 注解学习小案例,比较系统性学习注解并且应用实践。简单应用了运行期注解,通过注解实现了setContentView功能;简单应用了编译器注解,通过注解实现了防暴力点击的功能,同时支持设置时间间隔;使用注解替代枚举;使用注解一步步搭建简单路由案例。结合相应的博客,在来一些小案例,从此应该对注解有更加深入的理解……

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议或者问题,万事起于忽微,量变引起质变!

13.0.0.1 什么是注解?系统内置的标准注解有哪些?SuppressWarnings用过没?Android中提供了哪些与线程相关的注解?

  • 什么是注解?
    • Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息或者任何元数据(metadata)的途径和方法。基本规则:Annotation(注解)不能影响程序代码的执行,无论增加,删除Annotation(注解),代码都始终如一的执行。
  • 系统内置的标准注解有哪些?
    • @Override表示子类对父类方法的重写所带的注解;@Deprecated表示已经过时,不建议使用,但是依然可以使用;@SuppressWarnings("all")用来抑制编译时的警告信息,编译器在你写代码的时候,难免会出现很多的警告,有强迫症的程序猿会感到极其不爽,那么肿么办呢?@SuppressWarnings注解就可以告诉编译器,别警告啦,代码不会有问题的。
  • SuppressWarnings用过没?
    • @SuppressWarnings注解一定要传递一个参数给它,来表示过滤掉哪些类型的警告,笔者添加了”all”表示过滤掉所有类型的警告,很好理解吧!那么还可以传递什么参数来过滤警告呢?看看下面的表格你就会知道啦:
    • image
  • Android中提供了哪些与线程相关的注解?
    • @UiThread,通常可以等同于主线程,标注方法需要在UIThread执行,比如View类就使用这个注解
    • @MainThread 主线程,经常启动后创建的第一个线程
    • @WorkerThread 工作者线程,一般为一些后台的线程,比如AsyncTask里面的doInBackground就是这样的
    • @BinderThread 注解方法必须要在BinderThread线程中执行,一般使用较少

13.0.0.2 什么是apt?apt的难点和优势?什么是注解处理器?抽象处理器中四个方法有何作用?annotationProcessor和apt区别?

  • 什么是apt?
    • APT(Annotation Processing Tool的简称),可以在代码编译期解析注解,并且生成新的Java文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife等
  • apt的难点和优势?
    • 难点:就apt本身来说没有任何难点可言,难点一在于设计模式和解耦思想的灵活应用,二在与代码生成的繁琐,你可以手动字符串拼接,当然有更高级的玩法用squareup的javapoet,用建造者的模式构建出任何你想要的源代码
    • 优点:它的强大之处无需多言,看代表框架的源码,你可以学到很多新姿势。总的一句话:它可以做任何你不想做的繁杂的工作,它可以帮你写任何你不想重复代码。
  • 什么是注解处理器?
    • 注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。
    • 注解处理器可以生成Java代码,这些生成的Java代码会组成 .java 文件,但不能修改已经存在的Java类(即不能向已有的类中添加方法)。而这些生成的Java文件,会同时与其他普通的手写Java源代码一起被javac编译。
  • 抽象处理器中四个方法有何作用?
    • 每一个注解处理器都要继承于AbstractProcessor,如下所示:
      public class MyProcessor extends AbstractProcessor{
          @Override
          public synchronized void init(ProcessingEnvironment processingEnvironment){
              super.init(processingEnvironment);
          }
      
          @Override
          public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
              return false;
          }
      
          @Override
          public Set<String> getSupportedAnnotationTypes(){
              return super.getSupportedAnnotationTypes();
          }
      
          @Override
          public SourceVersion getSupportedSourceVersion(){
              return super.getSupportedSourceVersion();
          }
      }
      
    • 这几个方法如下
      • init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。后面我们将看到详细的内容。
      • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
      • getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
      • getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7的话,你也可以返回SourceVersion.RELEASE_7。我推荐你使用前者。
  • annotationProcessor和apt区别?
    • Android 官方的 annotationProcessor 同时支持 javac 和 jack 编译方式,而 android-apt 只支持 javac 方式。当然,目前 android-apt 在 Android Gradle 插件 2.2 版本上面仍然可以正常运行,如果你没有想支持 jack 编译方式的话,可以继续使用 android-apt。
  • 什么是jack编译方式?
    • Jack (Java Android Compiler Kit)是新的Android 编译工具,从Android 6.0 开始加入,替换原有的编译工具,例如javac, ProGuard, jarjar和 dx。它主要负责将java代码编译成dex包,并支持代码压缩,混淆等。
  • Jack工具的主要优势
    • 完全开放源码,源码均在AOSP中,合作伙伴可贡献源码
    • 加快编译源码,Jack 提供特殊的配置,减少编译时间:pre-dexing, 增量编译和Jack编译服务器.
    • 支持代码压缩,混淆,重打包和multidex,不在使用额外单独的包,例如ProGuard。

13.0.0.3 注解是怎么分类的?自定义注解又是怎么分类的?运行期注解原理是什么?

  • 注解是怎么分类的?
    • 标准 Annotation
      • 包括 Override, Deprecated, SuppressWarnings,是java自带的几个注解,他们由编译器来识别,不会进行编译, 不影响代码运行,至于他们的含义不是这篇博客的重点,这里不再讲述。
    • 元 Annotation
      • @Retention, @Target, @Inherited, @Documented,它们是用来定义 Annotation 的 Annotation。也就是当我们要自定义注解时,需要使用它们。
    • 自定义 Annotation
      • 根据需要,自定义的Annotation。而自定义的方式,下面我们会讲到。
  • 自定义注解又是怎么分类的?
    • 同样,自定义的注解也分为三类,通过元Annotation - @Retention 定义:
    • @Retention(RetentionPolicy.SOURCE)
      • 源码时注解,一般用来作为编译器标记。如Override, Deprecated, SuppressWarnings。
    • @Retention(RetentionPolicy.RUNTIME)
      • 运行时注解,在运行时通过反射去识别的注解。
      • 定义运行时注解,只需要在声明注解时指定@Retention(RetentionPolicy.RUNTIME)即可。
      • 运行时注解一般和反射机制配合使用,相比编译时注解性能比较低,但灵活性好,实现起来比较简答。
    • @Retention(RetentionPolicy.CLASS)
      • 编译时注解,在编译时被识别并处理的注解,这是本章重点。
      • 编译时注解能够自动处理Java源文件并生成更多的源码、配置文件、脚本或其他可能想要生成的东西。
  • 运行期注解原理是什么?
  • 实际注解案例有哪些?
    • 运行时注解:retrofit
    • 编译时注解:Dagger2, ButterKnife, EventBus3

13.0.0.4 在自定义注解中,Annotation里面的方法为何不能是private?Annotation里面的方法参数有哪些?

  • Annotation里面的方法为何不能是private?
    • 只能用public或默认(default)这两个访问权修饰.例如,String value();不能是private;因为它是提供给外部使用的。
    • image
  • Annotation里面的方法参数有哪些
    • 参数只能使用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数类型就为String; 

13.0.0.5 @Inherited是什么意思?注解是不可以继承的,这是为什么?注解的继承这个概念该如何理解?

  • @Inherited是什么意思?
    • 该注解的字面意识是继承,但你要知道注解是不可以继承的。@Inherited是在继承结构中使用的注解。
    • 如果你的注解是这样定义的:
      • 当你的注解定义到类A上,此时,有个B类继承A,且没使用该注解。但是扫描的时候,会把A类设置的注解,扫描到B类上。
      @Inherited
      @Retention(RetentionPolicy.CLASS)
      @Target(ElementType.TYPE)
      public @interface Test {
      	//...
      }
      
  • 注解是不可以继承的,这是为什么?
    • 注解不能继承
  • 注解的继承这个概念该如何理解?
    • 这里讲的继承并不是通过@Inherited修饰的注解。这个“继承”是一个注解的使用技巧,使用上的感觉类似于依赖倒置,来自于ButterKnife源码。
    • 这是ButterKnife的OnClick 注解。特殊的地方在于**@OnClick修饰了注解@ListenerClass**,并且设置了一些只属于@OnClick的属性。
    • 那这样的作用是什么呢?凡是修饰了@OnClick的地方,也就自动修饰了@ListenerClass。类似于@OnClick是@ListenerClass的子类。而ButterKnife有很多的监听注解@OnItemClick、@OnLongClick等等。这样在做代码生成时,不需要再单独考虑每一个监听注解,只需要处理@ListenerClass就OK。
    @Target(METHOD)
    @Retention(CLASS)
    @ListenerClass(
        targetType = "android.view.View",
    	setter = "setOnClickListener",
    	type = "butterknife.internal.DebouncingOnClickListener",
    	method = @ListenerMethod(
        	name = "doClick",
        	parameters = "android.view.View"
    	)
    )
    public @interface OnClick {
    	/** View IDs to which the method will be bound. */
    	int[] value() default { View.NO_ID };
    }
    

13.0.0.6 什么是依赖注入?依赖注入案例举例说明,有哪些方式,具备什么优势?依赖查找和依赖注入有什么区别?

  • 什么是依赖注入
    • 在面向对象编程中,经常处理处理的问题就是解耦,程序的耦合性越低表明这个程序的可读性以及可维护性越高。控制反转(Inversion of Control或IoC)就是常用的面向对象编程的设计原则,使用这个原则我们可以降低耦合性。其中依赖注入是控制反转最常用的实现。
  • 依赖注入案例举例说明,有哪些方式,具备什么优势?
    • 依赖是程序中常见的现象,比如类Car中用到了GasEnergy类的实例energy,通常的做法就是在Car类中显式地创建GasEnergy类的实例,并赋值给energy。如下面的代码
      interface Energy {
            
      }
        
      class GasEnergy implements Energy {
            
      }
        
      class Car {
          Energy energy = new GasEnergy();
      }
      
    • 存在问题
      • 类Car承担了多余的责任,负责energy对象的创建,这必然存在了严重的耦合性。举一个现实中的例子,一辆汽车使用哪种能源不是由汽车来决定,而是由汽车制造商(CarMaker)来决定,这是汽车制造商的责任。
      • 可扩展性,假设我们想修改能源为电动力,那么我们必然要修改Car这个类,明显不符合开放闭合原则。
      • 不利于单元测试。
    • 有哪些方式解耦
      • 依赖注入是这样的一种行为,在类Car中不主动创建GasEnergy的对象,而是通过外部传入GasEnergy对象形式来设置依赖。 常用的依赖注入有如下三种方式
    • 构造器注入
      • 将需要的依赖作为构造方法的参数传递完成依赖注入。
      class Car {
        Energy mEnergy;
        public Car(Energy energy) {
            mEnergy = energy;
        }
      }
      
    • Setter方法注入
      • 增加setter方法,参数为需要注入的依赖亦可完成依赖注入。
      class Car {
        Energy mEnergy;
            
        public void setEnergy(Energy energy) {
            mEnergy  = energy;
        }
      }
      
    • 接口注入
      • 接口注入,闻其名不言而喻,就是为依赖注入创建一套接口,依赖作为参数传入,通过调用统一的接口完成对具体实现的依赖注入。
      • 接口注入和setter方法注入类似,不同的是接口注入使用了统一的方法来完成注入,而setter方法注入的方法名称相对比较随意。
      interface EnergyConsumerInterface {
        public void setEnergy(Energy energy);
      }
        
      class Car implements EnergyConsumerInterface {
        Energy mEnergy;
            
        public void setEnergy(Energy energy) {
            mEnergy  = energy;
        }
      }
      
  • 依赖查找和依赖注入
    • 依赖查找和依赖注入一样属于控制反转原则的具体实现,不同于依赖注入的被动接受,依赖查找这是主动请求,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。

13.0.0.7 路由框架为何需要依赖注入,不用的话行不行?路由用什么方式注入,这些注入方式各具何特点,为何选择注解注入?

  • 路由框架为何需要依赖注入,不用的话行不行?
    • 路由的目的就是要实现跳转,但是你有没有想过,两个Activity之间的跳转肯定免不了要传入一些参数,如果我们在跳转后还要通过intent去获取参数,这样岂不是很麻烦,如果可以自动把参数赋给属性多好啊!
    • 组件化中两个module之间可能有一些功能并不需要跳转页面,如支付模块要获取用户模块的用户id,并不需要跳转页面,那么我们就要持有用户模块含有获取用户id功能的类的引用,如果我们在支付模块创建一个用户模块的功能引用,显然就违背了解耦的规则。这两个问题显然用依赖注入的方式会更好些,如果你用过ARouter,你会发现ARouter中的服务(IProvider)就是通过依赖注入实现的。
  • 路由用什么方式注入,这些注入方式各具何特点,为何选择注解注入?
    • 可以使用注解的方式,至于为啥,接下来我分析一下……
      • 为什么要用注解实现依赖注入,因为我们用了apt啊,那岂不是天生实现依赖注入的利器。如果你去写配置文件或构造方法等等,未免太复杂。
    • 无法通过构造方法注入【构造器注入】
      • 在多组件并行开发过程中,因为两个module没有引用关系,所以就不能通过构造方法传入要依赖的类,这个时候怎么办呢?连对方的引用都得不到,如何进行依赖呢?
    • 不要通过反射方式注入
      • 可能你会想到反射,我们先pass这个方案,能不用反射就能做好的前提下,我们最好不要用反射。
    • 不建议通过接口方式注入【也可以,但若是开源成lib,则不建议】
      • 可能有人会想到,在基类下沉一个接口功能标准,比如在基类库base模块定义获取用户id的接口,然后在用户模块实现接口的方法。那么当支付模块需要用到这个功能,就声明这个接口,然后一行注解通过框架为你创建实例,这样用户模块只需要提供功能,并不需要关心谁在用这个功能,这样岂不是大大减小了耦合。
      • 但是有一点,组件化实践中,有很多个模块,那岂不是所有的都需要依赖base基类库,才能使用到接口注入,这样不易转接。

13.0.0.8 实际开发中使用到注解有哪些,使用注解替代枚举?如何通过注解限定传入的类型?为何说枚举损耗性能?

  • 实际开发中使用到注解有哪些,使用注解替代枚举?
    /**
     * 播放模式
     * -1               播放错误
     * 0                播放未开始
     * 1                播放准备中
     * 2                播放准备就绪
     * 3                正在播放
     * 4                暂停播放
     * 5                正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
     * 6                正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
     * 7                播放完成
     */
    public @interface CurrentState{
        int STATE_ERROR = -1;
        int STATE_IDLE = 0;
        int STATE_PREPARING = 1;
        int STATE_PREPARED = 2;
        int STATE_PLAYING = 3;
        int STATE_PAUSED = 4;
        int STATE_BUFFERING_PLAYING = 5;
        int STATE_BUFFERING_PAUSED = 6;
        int STATE_COMPLETED = 7;
    }
    
  • 如何通过注解限定传入的类型?
    • 代码如下所示
    • 具体的案例,可以看我视频播放器开源库:github.com/yangchong21…
    • 枚举最大的作用是提供了类型安全。为了弥补Android平台不建议使用枚举的缺陷,官方推出了两个注解,IntDef和StringDef,用来提供编译期的类型检查。
    • 倘若,传入的值不是IjkPlayerType中的类型,则会导致编译提醒和警告。
     /**
     * 通过注解限定类型
     * TYPE_IJK                 IjkPlayer,基于IjkPlayer封装播放器
     * TYPE_NATIVE              MediaPlayer,基于原生自带的播放器控件
     */
    @Retention(RetentionPolicy.SOURCE)
    public @interface IjkPlayerType {
        int TYPE_IJK = 111;
        int TYPE_NATIVE = 222;
    }
    @IntDef({IjkPlayerType.TYPE_IJK,IjkPlayerType.TYPE_NATIVE})
    public @interface PlayerType{}
    
    
    //使用
    /**
     * 设置播放器类型,必须设置
     * 注意:感谢某人建议,这里限定了传入值类型
     * 输入值:ConstantKeys.IjkPlayerType.TYPE_IJK   或者  ConstantKeys.IjkPlayerType.TYPE_NATIVE
     * @param playerType IjkPlayer or MediaPlayer.
     */
    public void setPlayerType(@ConstantKeys.PlayerType int playerType) {
        mPlayerType = playerType;
    }
    

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客

03.12.注解练习案例开源代码