美文网首页
Android进阶 注解

Android进阶 注解

作者: 月止风溟 | 来源:发表于2018-07-03 22:53 被阅读0次

    Android进阶 注解

    1.注解是什么

    注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。
    注解有许多用处,主要如下:

    • 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
    • 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
    • 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取

    Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。

    Annotation(注解)是JDK5.0及以后版本引入的。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。从某些方面看,annotation就像修饰符一样被使用,并应用于包、类 型、构造方法、方法、成员变量、参数、本地变量的声明中。这些信息被存储在Annotation“name=value”结构对中。

    经过上面一些解释,我们来定义一个注解@FindWrong

    public @interface FindWrong {}
    
    public class SortZero {
        @FindWrong
        static void sort(int[] res) {}
    }
    

    这个时候我们发现IDE能否识别@FindWrong

    当然,上面那个例子IDE能够识别,但是它不能被使用。
    IDE傻傻的,它并没有检测注解定义的合理性。它差了哪些呢?

    我们常常这么写:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }
    

    查看一下常用的@Override

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

    其他注解类似。可以看到一个注解至少有两个元素,@Target@Retention
    也就是说,有几个注解是最基础的注解,被称之为元注解

    2.元注解

    元注解有四个,1.8添加一个到五个。

    1. @Target
        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Target {
            ElementType[] value();
        }
    

    表示该注解用于什么地方

     public enum ElementType {
        /** 类,接口(包括注解类型)或enum声明  */
        TYPE,
    
        /** 域声明(包括 enum 实例) */
        FIELD,
    
        /** 方法声明 */
        METHOD,
    
        /** 参数声明 */
        PARAMETER,
    
        /** 构造器声明 */
        CONSTRUCTOR,
    
        /** 局部变量声明  */
        LOCAL_VARIABLE,
    
        /** Annotation type declaration */
        ANNOTATION_TYPE,
    
        /**  包声明  */
        PACKAGE,
    
        /**
         * 类型参数声明
         *
         * @since 1.8
         */
        TYPE_PARAMETER,
    
        /**
         * 类型的使用
         *
         * @since 1.8
         */
        TYPE_USE
        }
    
    1. @Retention
        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Retention {
            RetentionPolicy value();
        }
    

    表示该注解可以保存的范围

        public enum RetentionPolicy {
        /**
         * 源代码:即此注解只能保存在源代码中
         * 当编译时,会被丢弃
         */
        SOURCE,
    
        /**
         * class文件:即此注解可以在class文件中保留
         * 但会被jvm丢弃
         */
        CLASS,
    
        /**
         * 运行期:即此注解可以在运行时保留
         * 可以通过反、反射获得
         */
        RUNTIME
        }
    
    1. @Documented
        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Documented {
        }
    

    即拥有这个注解的元素可以被javadoc此类的工具文档化。它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@return,@param 等。

    1. @Inherited
        @Documented
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.ANNOTATION_TYPE)
        public @interface Inherited {
        }
    

    允许子类继承父类中的注解。即拥有此注解的元素其子类可以继承父类的注解。

    1. @Repeatable
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.ANNOTATION_TYPE})
    public @interface Repeatable {
        Class<? extends Annotation> value();
    }
    

    Repeatable是可重复的意思。这个是JDK1.8新增的,表明一个注解可以多次应用。

    3.注解如何被使用

    在给出代码示例之前,要求大家

    • 对反射有一点点了解
    • isAnnotationPresent(Class<? extends Annotation> annotationClass)判断是不是有这个注解
    • getAnnotation(Class<T> annotationClass)获取注解对象
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface FindWrong {
    }
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class SortZero {
    
        @FindWrong()
        public void prit(){
            System.out.println("hello world");
        }
    
        public static void main(String[] args) {
            SortZero obj = new SortZero();
            Method[] methods = obj.getClass().getMethods();
            for(Method method: methods){
                if(method.isAnnotationPresent(FindWrong.class)){
                    try {
                        method.invoke(obj,null);
                    }catch (IllegalAccessException e){
                        e.printStackTrace();
                    }catch (IllegalArgumentException e){
                        e.printStackTrace();
                    }catch (InvocationTargetException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    如果我们要传参数怎么办,可以这样

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface FindWrong {
        String say();
    }
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class SortZero {
    
        @FindWrong(say = "hello world haha")
        public void prit(String str){
            System.out.println(str);
        }
    
        public static void main(String[] args) {
            SortZero obj = new SortZero();
            Method[] methods = obj.getClass().getMethods();
            for(Method method: methods){
                if(method.isAnnotationPresent(FindWrong.class)){
                    FindWrong value = method.getAnnotation(FindWrong.class);
                    try {
                        method.invoke(obj,value.say());
                    }catch (IllegalAccessException e){
                        e.printStackTrace();
                    }catch (IllegalArgumentException e){
                        e.printStackTrace();
                    }catch (InvocationTargetException e){
                        e.printStackTrace();
                    }
    
                }
            }
        }
    }
    

    注解的默认方法是value()

    注解参数的可支持数据类型

    • 1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
    • 2.String类型
    • 3.Class类型
    • 4.enum类型
    • 5.Annotation类型
    • 6.以上所有类型的数组

    3.我们不妨实现一下butterknife###

    通过以上的知识学习,我们不妨考虑一下一个简单的butterknife框架该如何实现。

    首先,我们事先成员变量的注解 @BindView

    package com.example.sleepydragon.myapplication;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface BindView {
        int value();
    }
    

    再事先一下方法的注解 @OnClick

    package com.example.sleepydragon.myapplication;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface OnClick {
        int[] id();
    }
    

    实现一下他们对应的绑定方法bind()

    package com.example.sleepydragon.myapplication;
    
    import android.app.Activity;
    import android.view.View;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class MyButterKnife {
        static void bind(final Activity activity){
            bindView(activity);
            bindOnClick(activity);
        }
    
        private static void bindOnClick(final Activity activity) {
            Method[] methods = activity.getClass().getMethods();
            for(Method method: methods){
                final Method thisMethod = method;
                if(thisMethod.isAnnotationPresent(OnClick.class)){
                    final OnClick value = thisMethod.getAnnotation(OnClick.class);
                    for(int id:value.id()){
                        final int thisId = id;
                        final View thisView = activity.findViewById(thisId);
                        thisView.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                try {
                                    thisMethod.invoke(activity,thisView);
                                }catch (IllegalAccessException e){
                                    e.printStackTrace();
                                }catch (IllegalArgumentException e){
                                    e.printStackTrace();
                                }catch (InvocationTargetException e){
                                    e.printStackTrace();
                                }
                            }
                        });
                    }
                }
            }
        }
    
        private static void bindView(final Activity activity) {
            Field[] fields = activity.getClass().getFields();
            for(Field field: fields){
                final Field thisFiled = field;
                if(thisFiled.isAnnotationPresent(BindView.class)){
                    final BindView value = thisFiled.getAnnotation(BindView.class);
                    thisFiled.setAccessible(true);
                    try {
                        thisFiled.set(activity,activity.findViewById(value.value())) ;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    我们写个方法测一下。

    package com.example.sleepydragon.myapplication;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.TextView;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.hello)
        TextView mTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            MyButterKnife.bind(this);
        }
    
        @OnClick(id={R.id.hello})
        public void click(View view){
            Toast.makeText(this,"o "+mTextView.toString(),Toast.LENGTH_LONG).show();
        }
    }
    

    请大家思考一下,实际工程中,能够用上面这种实现方式吗

    以上方式灵活,但是存在效率问题。通过反射去获取对象和直接生成对象现在有两到三倍的耗时差距。

    4.APT和annotationProcessor

    APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

    随着Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt ,并推荐大家使用 Android 官方插件annotationProcessor。

    事实上,你现在用apt,Androidstudio会警告你。

    5.怎么实现一个编译注解框架

    首先,我们了解一下编译注解框架的功能。他是在apk编译的时候,生成辅助的class文件,打包进对应的包,以实现相应功能。

    我说明一下,就不演示了。用ButterKnife标注一个view。编译之后他会在module_build_generated_source_apt_debug_包名_对应的Activity(或是其他)后面加上标志(_ViewBinding)的一个类。每次调用的ButterKnife.bind()就是触发这个类的构造函数,来进行绑定动作。

    为了达到类似效果,我们构建一个工程。

    工程结构

    javalib myanonation实现自己的注解
    javalib myprocessor实现自己的annotationProcessor。目的是根据注解生成对应class并注入。
    Androidlib mycompiler 实现自己的实际类与生成类的关联方式。

    我们来看一下各个类实现:
    与上面的BindView不同,现在我们申明它的生命周期是CLASS

    package com.phicomm.myanonation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
        int value();
    }
    

    关联类应该做什么呢?我们可以从ButterKnife抄一抄。下面是抽出来的主体方法。

    package com.phicomm.mycompiler;
    
    import android.app.Activity;
    import android.view.View;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    public class MyButterKnife {
    
        public static void bind(Activity activity){
            View sourceView = activity.getWindow().getDecorView();
            Class<?> targetClass = activity.getClass();
            Constructor<?> bindingCtor;
            try {
                //尝试找对应Activity叫上后缀_ViewBinding的类
                Class<?> bindingClass = targetClass.getClassLoader().loadClass(targetClass.getName() + "_ViewBinding");
                //找它的构造器
                bindingCtor = (Constructor<?>) bindingClass.getConstructor(activity.getClass(), View.class);
    
            }catch (ClassNotFoundException e) {
                throw new RuntimeException("Unable to find binding constructor for " + targetClass.getName(), e);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("Unable to find binding constructor for " + targetClass.getName(), e);
            }
            try {
                //通过构造器生成类
                bindingCtor.newInstance(activity, sourceView);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Unable to invoke " + bindingCtor, e);
            } catch (InstantiationException e) {
                throw new RuntimeException("Unable to invoke " + bindingCtor, e);
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                }
                if (cause instanceof Error) {
                    throw (Error) cause;
                }
                throw new RuntimeException("Unable to create binding instance.", cause);
            }
        }
    }
    
    

    关于anonationProcessor,它是生成对应类。我们先看看调用@BindView的Activity是什么样子。

    package com.phicomm.testanonation;
    
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.widget.TextView;
    
    import com.phicomm.myanonation.BindView;
    import com.phicomm.mycompiler.MyButterKnife;
    
    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.hello)
        TextView mHelloTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            MyButterKnife.bind(this);
            Log.e("xionglong",mHelloTextView.toString());
        }
    }
    

    那么,bind方法实现的功能其实就一句话。

        mHelloTextView = (TextView)findViewById(R.id.hello);
    

    我们其实就想把这个话放到生成的MainActivity_ViewBinding.class的构造函数里。大家可以考虑下怎么写这个构造函数。我这里给出一种方式。

    package com.phicomm.testanonation;
    
    import android.view.View;
    import com.phicomm.testanonation.MainActivity;
    
    public class MainActivity_ViewBinding{
        public MainActivity_ViewBinding(com.phicomm.testanonation.MainActivity host,android.view.View $view){
            host.mHelloTextView=(android.widget.TextView)$view.findViewById(2131165239);
        }
    }
    

    我们要手动生成这个class。注入操作有google的框架完成。
    首先,在myprocessorbuild.gradle里面添加google的框架。

    apply plugin: 'java-library'
    apply plugin: 'java'
    sourceCompatibility =JavaVersion.VERSION_1_8
    targetCompatibility =JavaVersion.VERSION_1_8
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation project(':myanonation')
        //AutoService 主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。
        implementation 'com.google.auto.service:auto-service:1.0-rc3'
    }
    

    生成下面这个文件就算好。需要有下面那个自定义的注解类。
    [图片上传失败...(image-5086c7-1530629574099)]

    然后,我们考虑一下生成辅助class文件哪些是必须的。
    在我们这个示例Demo中,有这些东西我们必须得知道:

    • MainActivity的包名com.phicomm.testanonation
    • MainActivity的类名MainActivity
    • 成员变量的mHelloTextView的名称mHelloTextView
    • 成员变量的mHelloTextView的类型android.widget.TextView
    • id我们可以通过注解的值拿到
    package com.phicomm.myprocessor;
    
    
    import com.google.auto.service.AutoService;
    import com.phicomm.myanonation.BindView;
    
    import java.io.IOException;
    import java.io.Writer;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Filer;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.Processor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.annotation.processing.SupportedAnnotationTypes;
    import javax.annotation.processing.SupportedSourceVersion;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.util.Elements;
    import javax.tools.Diagnostic;
    import javax.tools.JavaFileObject;
    
    @SupportedAnnotationTypes("com.phicomm.myanonation.BindView")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @AutoService(Processor.class)
    public class MyProcessor extends AbstractProcessor {
    
        //生成文件的帮助类
        private Filer mFiler;
        //Element的处理类,能够得到Element的各种属性
        private Elements mElementUtils;
        //编译时不能打出log,Messager辅助打出log
        private Messager mMessager;
    
        @Override
        public synchronized void init(ProcessingEnvironment env) {
            super.init(env);
            mFiler = env.getFiler();
            mElementUtils = processingEnv.getElementUtils();
            mMessager = processingEnv.getMessager();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
            mMessager.printMessage(Diagnostic.Kind.NOTE, "process...");
            // Process each @BindView element.
            for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
    
                int id = element.getAnnotation(BindView.class).value();
                String srcName = element.getSimpleName().toString();
    
                TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
                String qualifiedName = enclosingElement.getQualifiedName().toString();
                String simpleName = enclosingElement.getSimpleName().toString();
    
                String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();
    
                String typeName = element.asType().toString();
    
                try {
                    JavaFileObject sourceFile = mFiler.createSourceFile(
                            qualifiedName+"_ViewBinding", element);
                    Writer writer = sourceFile.openWriter();
                    writer.write(new StringBuilder()
                            .append("package "+packageName+";\n\n")
                            .append("import android.view.View;\n")
                            .append("import "+qualifiedName+";\n\n")
                            .append("public class "+simpleName+"_ViewBinding"+"{\n")
                            .append("    public "+simpleName+"_ViewBinding"+"("+qualifiedName+" host,android.view.View $view){"+"\n")
                            .append("        host."+srcName+"=("+typeName+")$view.findViewById("+id+");"+"\n")
                            .append("    }\n")
                            .append("}")
                            .toString());
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    error(element, "Unable to write binding for type %s: %s", element, e.getMessage());
                }
            }
            return false;
        }
    
        private void error(Element element, String message, Object... args) {
            printMessage(Diagnostic.Kind.ERROR, element, message, args);
        }
    
        private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
            if (args.length > 0) {
                message = String.format(message, args);
            }
            processingEnv.getMessager().printMessage(kind, message, element);
        }
    }
    

    有以上这些,我们的Demo就可以跑起来了。示例的Demo附在附件里。
    最后,我们考虑一下,Android的注解框架可以做到什么,不可以做到什么?
    ???

    附件demo

    参考文档:
    1.秒懂,Java 注解 (Annotation)你可以这样学
    2.注解Annotation实现原理与自定义注解例子
    3.APT注释工具
    4.使用编译时注解简单实现类似 ButterKnife 的效果

    相关文章

      网友评论

          本文标题:Android进阶 注解

          本文链接:https://www.haomeiwen.com/subject/irweyftx.html