关于Android注解这些基础,这些都不知道?历时半个月呕心之作

Posted 初一十五啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Android注解这些基础,这些都不知道?历时半个月呕心之作相关的知识,希望对你有一定的参考价值。

前言

前段时间去三亚旅游了一圈,玩是玩嗨了,也晒黑了。最后还被隔离了一段时间,无奈有点无聊,就静下心来收集内容来刷面试题。好在前前后后历时半个月,终于出土了😃

饭要一口一口吃,路要一步一步走。先从注解,泛型聊起。后续在聊性能优化再到架构,音视频,flutter等等。发车🏎️ 。

android面试题

💡 一丶架构设计核心-注解

视频:

架构师修炼之路-站在架构师的角度如何妙用自定义注解

1.1Android 注解入门以及自定义注解

Java 注解是JDK5.0 引入的一种注释机制。可以在类、方法、变量、参数和包都可以被标注。注解对注解的代码没有直接影响,之所以产生作用, 是对其解析后做了相应的处理。

JDK5.0内置了3个标准注解,4个元注解

  • @Override,检查方法是否是重写方法,如父类或引用的接口中没有该方法,则报编译报错。

  • @Desprecated,标记元素被废弃,使用该注解的元素,编译器会发出警告。

  • @SuppressWarnings,指示编译器忽略警告信息。

  • @Target,标记注解可用在什么地方(元注解)

  • @Retention,标记注解可用在什么地方(元注解)

  • @Documented,标记注解是否出现在 Javadoc 中(元注解)

  • @Inherited,标记标注的Annotation是否具有继承性(元注解)

在JDK7.0之后,额外增加了3个注解:

  • SafeVarargs,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告(Java 7支持)

  • FunctionalInterface,标识一个匿名函数或函数式接口(Java 8支持)

  • Repeatable,标识某注解可以在同一个声明上使用多次(Java 8支持)

定义注解

注解的通用定义如下,关键字@interface

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation 

定义一个注解,它的名字是 MyAnnotation,之后可以通过 @MyAnnotation 在代码中使用它。其中 @Documented@Target@interface 都是修饰 MyAnnotation的。

@MyAnnotation
public void annotationTest() 

@interface 定义注解时,意味着它实现了 java.lang.annotation.Annotation 接口,即该注解就是一个 Annotation,定义 Annotation 时,@interface是必须的。

@Documented 表示注解是否出现在 javadoc中,缺省则表示不出现在javadoc中。

@Target 作用指定Annotation的类型,如缺失@TargetzAnnotation可用于任何地方,否则用于指定的地方。

@Retention 作用指定Annotation的策略。

元注解

@MyAnnotation的注解中又引入了其它注解,如 @Target,@Retention,作用在其它注解上注解称为元注解

元注解有四个,@Target,@Retention,@Documented,@Inherited

@Target(ElementType.TYPE),标记注解可用在什么地方

package java.lang.annotation;

public enum ElementType 
    TYPE, //应用于接口、类、枚举、注解
    FIELD, //应用于属性字段、枚举常量
    METHOD, //应用于方法
    PARAMETER, //应用于方法的参数
    CONSTRUCTOR, //应用于构造函数
    LOCAL_VARIABLE, //应用于局部变量
    ANNOTATION_TYPE, //应用于注解类型@Retention注解中使用该属性
    PACKAGE, //应用于包声明
    TYPE_PARAMETER, //应用于类型泛型,即泛型方法、泛型类、泛型接口(jdk1.8)
    TYPE_USE //应用于类型使用,可用于出标注任意类型,除了class(jdk1.8)

@Retention(RetentionPolicy.RUNTIME),标记注解的保存策略

package java.lang.annotation;

public enum RetentionPolicy 
    SOURCE, //源文件,编译时丢弃,class字节码文件中不包含
    CLASS, //默认策略,编译时保留,class字节码中存在,运行时丢弃,运行时无法获得
    RUNTIME //编译时保留,运行时保留,可通过反射获得

@Documented,标记注解是否出现在 Javadoc 中,缺省不出现在javadoc

@Inherited,标记标注的Annotation是否具有继承性,类使用@Inherited修饰的注解,子类会继承该注解,注意接口或者父类的方法使用@Inherited修饰,子类不会继承这个注解。

@Override的实现如下所示,其中@TargetElementType.METHOD,表示@Override注释只可使用在方法上,不可使用在类、构造函数等上面。@RetentionRetentionPolicy.SOURCE表示@Override的生效阶段只在源代码阶段,编译时和运行时无意义。@Documented缺失,表示@Override注解不会出现在javadoc中。

package java.lang;

import java.lang.annotation.*;

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

1.2注解作用

作用一Annotation 具有"让编译器进行编译检查的作用"。例如 @Override@Deprecated @SuppressWarnings 都具有编译检查作用。

比如 某个方法被@Override标注,则意味着该方法会覆盖父类中的同名方法,如方法被 @Override 修饰,父类中没有同名方法,则编译器报错。

比如某个方法被@Deprecated标注,则意味着该方法不再被建议使用,如开发人员试图使用,编译器则会给出相应的提示信息。

作用二 :在编译和运行时解析和使用Annotation

获取和解析注解内容,做相应的逻辑处理,具体参考自定义注解代码实现。比如很多第三方框架的实现ButterKnift,EventBus等是基于此方法。

注解示例

自定义一个注解,命名为RoyAnnotation

@Retention(RetentionPolicy.RUNTIME)
@interface RoyAnnotation 
    String[] value() default "unknown";

使用RoyAnnotation修饰testMethod方法

class TestAnnotation 
    @RoyAnnotation(value = "hello", "world")
    public void testMethod() 

    

运行时解析注解信息,做相应的逻辑处理

Class<TestAnnotation> testAnnotationClass = TestAnnotation.class;
Method method = testAnnotationClass.getMethod("testMethod");
if (method.isAnnotationPresent(RoyAnnotation.class)) 
    Method method = testAnnotationClass.getMethod("testMethod");
    if (method.isAnnotationPresent(RoyAnnotation.class)) 
        RoyAnnotation royAnnotation = method.getAnnotation(RoyAnnotation.class);
        String[] values = royAnnotation.value();
        for (String str:values) 
            System.out.printf(str+", ");
       
    


Annotation[] annotations = method.getAnnotations();
for(Annotation annotation: annotations) 
    System.out.println(annotation);

运行时注解

运行时注解,@Retention设置为RetentionPolicy.RUNTIME,需要注意的是,JVM 运行时通过反射注解处理的方式会对性能造成影响,日常开发中很少使用这样的方式。

通常获取组件通过findViewById方式,如果引用组件比较多,需要写大量的重复代码:

public TextView textView = findViewById(R.id.showInfo);

可以通过注解,实现Android依赖注入,定义一个注解@BindView:

@Retention(RetentionPolicy.RUNTIME)
public @interface BindView 
    int value();

创建注解处理器

public class InjectUtils 
    public static void inject(Activity activity) throws IllegalAccessException 
        Field[] fields = activity.getClass().getDeclaredFields();
        for (Field field : fields) 
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) 
                int value = bindView.value();
                View view = activity.findViewById(value);
                try 
                    field.set(activity, view);
                 catch (IllegalAccessException e) 
                    e.printStackTrace();
                
            
        
    

activity中使用方式:组件添加@BindView注解,调用InjectUtils.inject(this)通过反射处理逻辑

public class MainActivity extends AppCompatActivity  

    @BindView(R.id.showInfo)
    public TextView textView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        try 
            InjectUtils.inject(this);
         catch (IllegalAccessException e) 
            e.printStackTrace();
        
        textView.setText("helloWorld");
    

编译时注解

编译时注解的核心是APT(Annotation Processing Tools),原理是代码中设置注解,在编译时检查AbstractProcessor子类,并调用该类型的process函数,将所有添加注解的元素传递到process中,在process做相应逻辑处理。

创建一个注解处理器MyProcessor,首先需要继承AbstractProcessor类,AbstractProcessor类只在Java中有,Android库不存在,在项目中添加一个module,命名为processor,选择module时候需要选择Java。

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.processor.MyAnnotation")
@AutoService(MyProcessor.class)
public class MyProcessor extends AbstractProcessor

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        //processingEnv提供工具类
        super.init(processingEnv);
    
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
        //注解解析,以及生成Java文件
        return false;
    

@SupportedSourceVersion是支持的Java版本

@SupportedAnnotationTypes是支持的注解

@AutoService是自动生成相应的配置

使用@AutoService Gradle引入:implementation 'com.google.auto.service:auto-service:1.0-rc7'

APT始于jdk5,止于jdk8,自jdk6,可以使用Pluggable Annotation Processing API 替换它。

1.3APT实现原理

APT是什么?
APT可以在android编译期动态生成预设好格式的java文件

APT相比注解的优势
APT产生的原因是注解的低效率,APT主要解决的是注解+反射执行慢的问题,APT通过预设代码动态生成java代码文件,直接调用java对象,从而替换掉遍历查找注解的耗时操作,在Android这种执行效率不高的移动端,性能提升十分明显

APT可以做到什么?
减少模板代码:可以利用APT在编译期动态生成java代码,省去很多模板代码,节省很多编码量
解耦合,让代码更灵活:可以利用辅助类+反射的形式,做到类与类之间的解耦合,从而减少类之间的直接依赖

哪些框架在使用APT
Butterknife、Dagger2、DataBinding、EventBus3、ARouter

使用APT的步骤

  • 创建自定义注解

  • 创建注解处理器 继承自AbstractProcessor ,生成处理注解逻辑的.java文件,封装一个供外部调用的API接口,也就是调用第二步中生成的注解处理逻辑文件中的方法,实现逻辑处理

  • 然后在需要使用的Module中依赖注解,使用自定义注解处理器,然后调用API接口即可

下面我们以手写实现一个简易的Butterknife 为例,来实际使用一下APT, 我们这里主要以通过注解的方式实现findViewById()的操作;

创建自定义注解

我们首先创建一个Java Library Module,名称为AnnotationModule,在该Module下定义一个注解@MQBindView

 @Target(ElementType.FIELD)
      @Retention(RetentionPolicy.SOURCE)
      public @interface MQBindView 
         int value();
       

自定义注解处理器

创建一个Java Library Module,名称为APTModule,定义一个类继承自AbstractProcessor;
这里自定义处理器里面会用到我们自定义的注解,所以这个Module的build.gradle里面要依赖AnnotationModule

dependencies 
       implementation project(path: ':AnnotationModule')
      
public class MQProcessor extends AbstractProcessor 

       @Override
       public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 
    
       
    
    //重写getSupportedAnnotationTypes方法,添加支持处理的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() 
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(MQBindView.class.getCanonicalName());
        return supportTypes;
    
    
    @Override
    public SourceVersion getSupportedSourceVersion() 
        return SourceVersion.latestSupported();
    
      
      

自定义注解处理器一定要重写getSupportedAnnotationTypes方法,添加需要处理的注解类型

在编写注解处理逻辑之前,这里我先对Processor中的一些基本概念进行一下了解;

我们的app Module中如果依赖了这个APTModule,当我们如果build(编译)项目的时候,这个Processorprocess方法就会被执行;

当我们想要在这个Processor中添加一些打印信息,我们需要用Messager(javax.annotation.processing.Messager)去打印,打印出来的内容,在build的时候,会在下方的build视图中打印出来,这个build视图,正常会显示的是编译的过程信息,我们使用Messager打印的信息,也会在这里打印出来;

public class MQProcessor extends AbstractProcessor 
        

        Messager mMessager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) 
    
        super.init(processingEnv);
    
        mMessager = processingEnv.getMessager();
    
        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->init");
    
        


          @Override
          public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 
    
           mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process");
    
          
    
       


Element

Element代表程序的一个元素,这个元素可以是:包、类/接口、属性变量、方法/方法形参、泛型参数,Elementjava-apt( 编译时注解处理器)技术的基础,在编译期间可以获取元素的各类信息,结合APT技术动态生成代码实现相应的逻辑处理;

Element接口中的一些方法:

getEnclosingElement()

获取包含该Element的父Element

public class MainActivity extends AppCompatActivity 

        @MQBindView(R.id.tv_activity_main)
        TextView mTextView;
    
        

这里,当我们拿到使用了@MQBindView这个注解的Element(mTextView),然后通过getEnclosingElement()方法就能获取到父Element,这里父Element就是MainActivity

process方法中,有一个参数RoundEnvironment,通过这个参数可以获取包含特定注解的被注解元素.

Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MQBindView.class);

init方法中有一个ProcessingEnvironment参数,通过这个参数可以获取元素所在的包

String packageName = mProcessingEnv.getElementUtils().getPackageOf(element).toString();

Filer

Filer是文件生成器,可以用来生成Java源文件等

JavaFileObject sourceFile = mFiler.createSourceFile(文件名);

这里文件名,要是创建的类的全类名

JavaFileObject sourceFile = mFiler.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding"
                );

这样创建文件之后,编译项目之后,会在app/build/generated/ap_generated_sources/debug/out/应用包名/下生成这个Java文件

Writer

我们创建Java源文件对象 JavaFileObject之后可以通过openWriter()方法获取文件的Writer对象,然后就可以通过Writer对象往文件里面写内容

而我们要实现Butterknife一样的通过注解的形式,帮我们实现findViewById()的功能,我们查看Butterknifer就知道实际上Butterknife就是使用了APT技术,在编译时给每一个使用了@BindView注解的类都生成了一个对应的Java源文件,名字为使用了注解的类的类名加上 _ViewBinding,比如我们的MainActivity使用了注解@BindView,则编译之后会生成MainActivity_ViewBinding这个Java源文件,生成的Java源文件就在app/build/generated/ap_generated_sources/debug/out/应用包名/ 这个目录下,也即新建了一个MainActivity_ViewBinding类,这个类的构造方法里面就会对MainActivtiy里面的所有使用了@BindView的View成员进行findViewById的操作,如下图所示

ButterKnife通过APT生成的Java源文件

Buterknife.bind(Activity)这个方法的实现如下


查看Butterknife的源码可知,Butterknife.bind(Activity)的实现,就是利用反射通过Class获取生成的Java源文件中定义的类的Class对象,然后获取Class的构造方法,通过反射,创建生成的Java类的对象,从而触发构造方法,触发findViewById相关代码;

了解了Butterknife的实现原理,那我们自己就可以照着这个逻辑去实现;

我们在自己自定义的注解处理器的process方法中,实现给每一个使用了@MQBindView的注解的类都生成一个对应的Java源文件,Java 源文件中的内容,为定义一个对应的类,类中的构造方法需要一个Activtiy参数,构造方法中的内容为给类中每一个使用了@BindView注解的成员进行findViewById的操作;

public class MQProcessor extends AbstractProcessor 

    private static final String TAG = "MQProcessor";
    
    Filer mFiler;
    ProcessingEnvironment mProcessingEnv;
    Messager mMessager;
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mProcessingEnv = processingEnv;
        mMessager = processingEnv.getMessager();
        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->init");
    
    
    @Override
    public Set<String> getSupportedAnnotationTypes() 
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(MQBindView.class.getCanonicalName());
        return supportTypes;
    
    
    @Override
    public SourceVersion getSupportedSourceVersion() 
        return SourceVersion.latestSupported();
    
    
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 
    
        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process");
    
        //我们的源代码中所有使用了@MQBindView注解的元素
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MQBindView.class);
    	//定义一个Map集合,以每一个Activity类名为键,这个Activtiy类中使用了@MQBindViwe注解的元素列表为值	
        Map<String, List<VariableElement>> map = new HashMap<>();
    
        for (Element element : elementsAnnotatedWith) 
    
            VariableElement variableElement = (VariableElement) element;
    
            //Activity的名字
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
    
            List<VariableElement> variableElements;
    
            if (map.get(activityName) != null) 
                variableElements = map.get(activityName);
                variableElements.add(variableElement);
             else 
                variableElements = new ArrayList<>();
                variableElements.add(variableElement);
                map.put(activityName, variableElements);
            
    
        
    
        Writer writer = null;
        
    	//给每一个使用了@MQVindView注解的类(Activity/Fragment等)都生成对应的Java源文件
        for (Map.Entry<String, List<VariableElement>> stringListEntry : map.entrySet()) 
    
            List<VariableElement> variableElementList = stringListEntry.getValue();
    
            TypeElement enclosingTypeElement = (TypeElement) variableElementList.get(0).getEnclosingElement();
    
            String packageName =
                    mProcessingEnv.getElementUtils().getPackageOf(enclosingTypeElement).toString();
    
            String activityName = variableElementList.get(0).getEnclosingElement().getSimpleName().toString();
    
            try 
                mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:" + mFiler);
    
                JavaFileObject sourceFile = mFiler.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding"
                );
    
         		/**
         		* Java源文件里的内容为定义一个类,类名就为使用了@BindView的注解的Activity的类名加_ViewBinding
         		* 然后构造方法有一个Activity类型的target参数,构造方法里面给这个Activity中使用了@BindView注解                的View成员进行dinfViewByid操作 
         		*
         		*/
                writer = sourceFile.openWriter();
                writer.write("package " + packageName + ";\\n");
                writer.write("import android.view.View;\\n");
                writer.write("public class " + activityName + "_ViewBinding \\n");
                writer.write("public " + activityName + "_ViewBinding (" + activityName + " target)\\n");
                writer.write("View decorView = target.getWindow().getDecorView();\\n");
    
                for (VariableElement variableElement : variableElementList) 
                    String variableName = variableElement.getSimpleName().toString();
                    int value = variableElement.getAnnotation(MQBindView.class).value();
                    writer.write("target." + variableName + "=decorView.findViewById(" + value + ");\\n");
                
                writer.write("\\n");
                writer.write("\\n");
                try 
                    mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:" + writer);
    
    				//最终Writer这个写对象,要关闭,否则内容是不会写进创建的Java源文件中的	
                    writer.close();
                 catch (IOException exception) 
                    exception.printStackTrace();
                
             catch (Exception exception) 
                exception.printStackTrace();
            
        
    
        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:end");
        return false;
    


配置注解处理器

自定义处理器代码编写好之后,还有一个非常重要的步骤,就是配置注解处理器
一共有两种方式:

第一种:

在自定义注解处理器所在的Module,APTModule的src/main目录下新建一个 resources/META-INF/services
三级目录,然后在services目录下新建一个名字为javax.annotation.processing.Processor的文件
文件里面的内容,写上自定义处理器的全类名即可.

第二种:

通过依赖google的auto-services库,帮我们自动生成META-INF/services/javax.annotation.processing.Processor 文件;
在自定义处理器所在的Module,APTModulebuild.gradle添加auto-services依赖

dependencies 
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    
    implementation project(path: ':AnnotationModule')

然后在我们编写的MQProcessor上方使用@AutoService注解

然后编译代码,我们可以看到在APTModule的build/classess/java/main/下会生成 META-INF/services/javax.annotation.processing.Processor文件,文件里面的内容就是com.example.aptmodule.MQProcessor

所以也就成功配置了依赖处理器,然后我们build一下项目,查看app/build/generated/ap_generated_sources/debug/out/应用包名/下成功生成了对应的Java源文件,表示注解处理器生效了.

定义一个使用入口,给使用者调用

我们这里定义了一个MQButterKnife类,提供了一个静态方法bind(Activity), bind 方法里面就是通过反射创建Activity对应生成的 Activity_ViewBinding对象触发findViewById操作

public class MQButterKnife 

    private static final String TAG = "MQButterKnife";
    
    public static void bind(Activity activity) 
    
        String viewBindingClassName =
                activity.getClass().getCanonicalName() + "_ViewBinding";
        Log.e(TAG, "bind:" + viewBindingClassName);
        try 
            Class<?> aClass = Class.forName(viewBindingClassName);
            Log.e(TAG, "bind: " + aClass);
            Constructor<?> constructor = aClass.getConstructor(activity.getClass());
            Object o = constructor.newInstance(activity);
            Log.e(TAG, "bind: " + o);
         catch (Exception e) 
            e.printStackTrace();
        
    
    


使用

在app主Module里面先依赖AnnotationModule,以及使用APTModule注解模块,在主Module的build.gradle里面添加如下,注意自定义处理器模板的依赖方式是通过annotationProcessor

dependencies 

    implementation project(path: ':AnnotationModule')
    
    annotationProcessor project(':APTModule')


然后在我们的Activty中去使用我们自定义的MQButterknife,实现View的findViewById操作

 public class MainActivity extends AppCompatActivity 

    @MQBindView(R.id.tv_activity_main)
    TextView mTextView;
    private static final String TAG = "MainActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) 
    
        super.onCreate(savedInstanceState);
    
        setContentView(R.layout.activity_main);
    
        MQButterKnife.bind(this);
    
        mTextView.setText("测试");
    
    


运行项目,mTextView成功显示 “测试”,说明我们的简单版本Bunnerknife成功实现了.

这样我们就成功的在Android项目中利用APT编译时注解处理技术实现了一个简单版本的Butterknife,也对APT技术的使用流程有了一定的了解;

以上是关于关于Android注解这些基础,这些都不知道?历时半个月呕心之作的主要内容,如果未能解决你的问题,请参考以下文章

2022年创业项目排行榜前十名,这些你都不知道就别想赚钱了

java之面向对象

面向对象思想的引入

赶紧收藏!这些Java中的流程控制知识你都不知道,你凭什么涨薪?

赶紧收藏!这些Java中的流程控制知识你都不知道,你凭什么涨薪?

C语言知识这些知识你都不知道?难怪说你学不懂C语言