侧边栏壁纸
博主头像
慢行的骑兵博主等级

贪多嚼不烂,欲速则不达

  • 累计撰写 29 篇文章
  • 累计创建 27 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

注解与注解处理器

慢行的骑兵
2021-09-07 / 0 评论 / 0 点赞 / 359 阅读 / 5,282 字

一.前言

  • 在学习和项目实战中,注解是必须要学习的,另外,面试的时候也可能会被问到。那么,注解在哪些方面有体现呢?简单的做一下总结,如下:

    • 1、注解结合Apt用于生成一些Java文件的框架,有:ButterKnife、Dagger2、Hilt、Databinding等;
    • 2、注解结合代码埋点,如Arouter、aspactJ等;
    • 3、注解结合反射的方式,如Xutils框架。还有Lifecycle,其核心原理就是使用注解结合反射的方式实现;
    • 4、在某些场景下,替代枚举的使用,可以实现优化内存,提高性能。如限定方法的参数传递;
      • 这里做个简述,后续有机会再研究。枚举的每一个成员实际上就是一个Object,是非常占用内存的(可以通过转成字节码指令集来查看),一个对象往往要占用12个对象头,还有对象的属性的内容,最后还有8字节对齐[涉及到了Jvm相关,先记录,后续有机会研究]。
  • 温馨提示:本篇文章的重点是注解的作用,使用,以及Demo(简单一句话,把demo看明白就可以了,难点在于对动态代理的理解,我会把之前整理的动态代理笔记重新整理一篇文章出来),其它的知识作为补充。先学习重点,再学习其它涉及的知识,该文章目的是便于复习。对于提及到的一些知识点,会选择性(是否有必要)的根据实际情况(精力是否够)来逐步的整理成新的文章。

二.注解

2.1.作用

  • 注解(Annotation)主要用于提升软件的质量和提高软件的生产效率。

2.2.意义

  • 注解本身没有任何意义,单独的注解就是一种注释,他需要结合其他如反射、插桩等技术才有意义。
  • Java 注解(Annotation)又称 Java 标注,是 JDK1.5引入的一种注释机制。是元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。

2.3.分类

  • 根据功能和用途分(也可以通过成员个数分类,该种略)
    • 系统内置注解:系统自带的注解类型,如@Override;
    • 元注解:注解的注解,负责注解其他注解,如@Target;
    • 自定义注解:用户根据自己的需求自定义的注解类型;
  • 要求:元注解的作用以及自定注解要熟悉使用,多写就可以了;

2.3.1.元注解

  • 这个还是单独列出来(需要识记)
  • 在定义注解时,注解类也能够使用其他的注解声明。在JDK1.5中提供了用来对注解类型进行注解的注解类,我们称之为 meta-annotation(元注解)
  • 声明的注解允许作用于哪些节点使用@Target(注解可以写在哪些地方)声明;保留级别由@Retention(注解在什么地方有效)声明。其中保留级别如下。
    • RetentionPolicy.SOURCE :标记的注解仅保留在源级别中,并被编译器忽略。
    • RetentionPolicy.CLASS :标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
    • RetentionPolicy.RUNTIME: 标记的注解由 JVM 保留,因此运行时环境可以使用它。
    • SOURCE < CLASS <RUNTIME,即CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS。

2.4.语法

  • 这个得练习,语言描述比不上代码实战;
  • 拿自定义注解举例
//这里需要添加元注解@Target和@Retention
public @interface 自定义注解名{
	//略
}
  • 注意:元注解的使用。
    • 注解只能用在哪些位置
    • 注解的作用域

2.5.注解有哪些生命周期

  • 不能按照Activity的生命周期方式去理解,这个考察点就是@Retention(注解在什么地方有效,作用域范围),如果定义为RetentionPolicy.SOURCE,是在源码阶段有效。若定义为RetentionPolicy.CLASS ,是在编译阶段有效。若定义为RetentionPolicy.RUNTIME,是在运行阶段有效。
  • 简单总结,注解的生命周期或作用域范围:源码阶段、编译阶段、运行时阶段

2.6.注解的应用场景

  • 在前言中,我们站在Android开发的角度去理解注解的应用场景,该位置通过注解的保留级别不同来归纳一下注解的应用场景。注解作用于:源码、字节码与运行时的应用场景
级别 技术 使用场景
源码 APT 在编译期能够获取注解与注解声明的类(包括类中所有成员信息),一般用于生成额外的辅助类。
字节码 字节码增强 在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
运行时 反射 在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定。
  • 字节码增强(补充,有印象即可,有精力再研究-美团技术团队的文章-字节码增强技术探索):字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成或修改符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

三.Apt

  • 全称Annotation Processing Tool,它是javac的一个工具,中文意思为编译时注解处理器;

3.1.作用

  • 简单理解:(通过Javac命令可以把Java源文件编译成字节码文件,)在代码的编译期去解析注解,(通过注解可以加入一些代码,把编译的过程做一些修改。)并且生成新的 Java 文件,减少手动的代码输入。

四.反射

4.1.反射机制

  • 反射详细(相对性的详细,不会全面的去总结各种api,没有必要,需要的时候去查询就行)总结起来会影响文章的篇幅,这里就简单的总结一下。
  • 一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。这时候,我们使用 JDK 提供的反射 API 进行反射调用。反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。是Java被视为动态语言的关键。
  • 再附上一张图(根据需要查询即可)

反射的知识点

4.2.反射机制提供的功能

  • 在运行时判断任意一个对象所属的类

  • 在运行时构造任意一个类的对象

  • 在运行时判断任意一个类所具有的成员变量和方法

  • 在运行时获取泛型信息

  • 在运行时调用任意一个对象的成员变量和方法

  • 在运行时处理注解

  • 生成动态代理

4.3.反射为什么慢(缺点)

  • 之前的笔记并未单独做这方面的总结,这里补充一下。
1、Method#invoke 需要进行自动拆装箱
	invoke 方法的参数是 Object[] 类型,如果是基本数据类型会转化为Integer装箱,同时再包装成Object数组。在执行时候又会把数组拆解开,并拆箱为基本数据类型。
2、反射需要按名检索类和方法
	http://androidxref.com/9.0.0_r3/xref/art/runtime/mirror/class.cc#1265
3、需要检查方法
	反射时需要检查方法可见性以及每个实际参数与形式参数的类型匹配性
4、编译器无法对动态调用的代码做优化,比如内联
	反射涉及到动态解析的类型,影响内联判断并且无法进行JIT
  • 反射的优点和缺点需要掌握;

五.动态代理的实现

  • 因为注解,反射,动态代理它们之间的关联很强,特别是在Retrofit框架中的体现,在此有必要将核心(简单)提及一下,如果需要去理解动态代理,则需要额外下些功夫了。
  • 在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。这个代理对象是存在于内存中的。
  • 简单理解:
//创建一个与代理对象相关联的InvocationHandler
 1、InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);
//创建一个代理对象stuProxy,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
 2、Person stuProxy= (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);

六.Demo

1.仿ButterKnife的BindView注解功能

  • 贴核心代码,具体源码请看
  • 源代码
package com.code.annotation_compiler;

import com.code.annotations.BindView;
import com.google.auto.service.AutoService;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaFileObject;

//@AutoService注解:作用是用来生成META-INF/services/javax.annotation.processing.Processor文件
@AutoService(Processor.class)
public class AnnotationsCompiler extends AbstractProcessor {

    //这个方法非常简单,只有一个返回值,用来指定当前正在使用的Java版本,通常return SourceVersion.latestSupported()即可。
    //1.支持的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //这个方法的返回值是一个Set集合,集合中指要处理的注解类型的名称(这里必须是完整的包名+类名,例如com.example.annotation.BindView)。由于在本例中只需要处理@BindView注解,因此Set集合中只需要添加@BindView的名称即可。
    //2.能用来处理哪些注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

    //3.定义一个用来生成APT目录下面的文件的对象
    Filer filer;

    //这个方法用于初始化处理器,方法中有一个ProcessingEnvironment类型的参数,ProcessingEnvironment是一个注解处理工具的集合。它包含了众多工具类。
    //例如:
    //Filer可以用来编写新文件;
    //Messager可以用来打印错误信息;
    //Elements是一个可以处理Element的工具类。
        //这个需要了解一下,具体请参考上方博客链接,写得非常好
        //重点:不同类型Element其实就是映射了Java中不同的类元素
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    //4.最重要的一个方法
    //在这个方法的方法体中,我们可以校验被注解的对象是否合法、可以编写处理注解的代码,以及自动生成需要的java文件等
    //我们要处理的大部分逻辑都是在这个方法中完成
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        //补充:这个方法执行了3次(具体原因不清楚)         在javac编译的时候回调该方法
        //javac源码分析
        //1.process是怎么回调的
        	//SPI机制
        //2.调用的次数是怎么决定的
        	//和是否生成文件有关系
        //3.返回值有什么用
        	//注解是否往下传递,true表示不传递set
        
        //4.2.获取APP中所有用到了BindView注解的对象
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        //重复一下:不同类型Element其实就是映射了Java中不同的类元素
        //PackageElement          表示一个包程序元素。提供对有关包及其成员的信息的访问。
        //TypeElement 类           表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
        //VariableElement 属性     表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
        //ExecutableElement 方法   表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。

        //4.3.开始对elementsAnnotatedWith进行分类

        //4.3.1.容器
        Map<String, List<VariableElement>> map = new HashMap<>();

        //4.3.2.扫描所有被@BindView注解的元素
        for (Element element : elementsAnnotatedWith) {
            //因为我们知道BindView元素的使用范围是在域上,所以这里我们进行了强制类型转换
            VariableElement variableElement = (VariableElement) element;
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
            List<VariableElement> variableElements = map.get(activityName);
            if (variableElements == null) {
                variableElements = new ArrayList<>();
                map.put(activityName, variableElements);
            }
            variableElements.add(variableElement);
        }

        //4.3.3.开始生成文件

        //如:
        //package com.jack.a2021_09_06day01;
        //import com.jack.a2021_09_06day01.JBinder;
        //public class MainActivity_ViewBinding implements JBinder<com.jack.a2021_09_06day01.MainActivity> {
        //    @Override
        //    public void bind(com.jack.a2021_09_06day01.MainActivity target) {
        //        target.mTextView = (android.widget.TextView) target.findViewById(2131231003);
        //    }
        //}

        if (map.size() > 0) {
            Writer writer = null;
            Iterator<String> iterator = map.keySet().iterator();
            while (iterator.hasNext()) {
                String activityName = iterator.next();
                List<VariableElement> variableElements = map.get(activityName);

                //得到包名
                TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement();
                String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();

                try {
                    JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + activityName + "_ViewBinding");
                    writer = sourceFile.openWriter();

                    //        package com.jack.a2021_09_06day01;
                    writer.write("package " + packageName + ";\n");
                    //        import com.jack.a2021_09_06day01.JBinder;
                    writer.write("import " + packageName + ".JBinder;\n");
                    //        public class MainActivity_ViewBinding implements JBinder<com.jack.a2021_09_06day01.MainActivity> {
                    writer.write("public class " + activityName + "_ViewBinding implements JBinder<" +
                            packageName + "." + activityName + ">{\n");
                    //            @Override
                    //            public void bind(com.jack.a2021_09_06day01.MainActivity target) {
                    writer.write(" @Override\n" +
                            " public void bind(" + packageName + "." + activityName + " target){");
                    //target.mTextView = (android.widget.TextView) target.findViewById(2131231003);
                    for (VariableElement variableElement : variableElements) {
                        //得到名字
                        String variableName = variableElement.getSimpleName().toString();
                        //得到ID
                        int id = variableElement.getAnnotation(BindView.class).value();
                        //得到类型
                        TypeMirror typeMirror = variableElement.asType();
                        writer.write("target." + variableName + "=(" + typeMirror + ")target.findViewById(" + id + ");\n");
                    }

                    writer.write("\n}}");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (writer != null) {
                        try {
                            writer.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

        //4.1.返回值表示注解是否由当前Processor 处理
        return false;           //记录一下:这里测试true或false,没看到区别
    }

}

2.仿ButterKnife的OnClick注解功能

package com.jack.a2021_09_11day01;

import android.view.View;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @创建者 Jack
 * @创建时间 2021/9/11 15:34
 * @描述
 */
public class InjectUtils {
    public static void inject(Object context) {
        injectClick(context);
    }

    private static void injectClick(Object context) {
        Method valueMethod = null;

        try {
            //最终的实现是 对context中的View设置点击事件的监听或长按监听

            //这里的context是activity,由于是学习思想(请忽略健壮性)
            Class<?> clazz = context.getClass();
            //获取activity中所有的方法
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                //遍历每一个方法
                //获取方法上的注解
                Annotation[] annotations = method.getAnnotations();
                for (Annotation annotation : annotations) {
                    //获取对应类型的注解的Class对象    OnClick或OnLongClick
                    Class<?> annotationClass = annotation.annotationType();

                    //获取OnClick或OnLongClick的EventBase元注解
                    EventBase eventBase = annotationClass.getAnnotation(EventBase.class);
                    if (eventBase == null) {
                        continue;
                    }

                    //是EventBase类型注解,则进行下一步
                    //获取EventBase中定义的三要素
                    //1.setOnClickListener 订阅关系
                    //String listenerSetter();
                    String listenerSetter = eventBase.listenerSetter();
                    //2.new View.OnClickListener()  事件本身
                    // Class<?> listenerType();
                    Class<?> listenerType = eventBase.listenerType();

                    //3.事件处理程序
                    //String callbackMethod();          这个在后面用不上
                    //                String callBackMethod = eventBase.callbackMethod();

                    //拿到value方法(自定义注解OnClick或OnLongClick中的value方法)
                    valueMethod = annotationClass.getDeclaredMethod("value");

                    //反射执行annotation的value方法,返回值是int[],放的是View的id
                    //invoke的方式执行value方法,获取MainAcitivy中的标有@OnClick或@OnLongClick中注解的括号内的参数id如:({R.id.btn1,R.id.btn2})
                    int[] viewId = (int[]) valueMethod.invoke(annotation);

                    //id进行遍历
                    for (int id : viewId) {
                        //通过反射拿到activity中的findViewById方法
                        Method findViewById = clazz.getMethod("findViewById", int.class);
                        //执行context(就是activity)中的findViewById方法,参数是id
                        View view = (View) findViewById.invoke(context, id);
                        if (view == null) {
                            continue;
                        }

                        //接下来需要使用到动态代理了
                        //这里是难点的开始
                        //先简单介绍一下动态代理的原理
                        //实现动态代理需要用到PRoxy类和InvocationHandler接口,按照固定规则(先按照要求传递参数,具体为何要如此传递,研读源码便可以知晓),最终会生成代理类。
                        //代理类的任何方法的执行都会触发InvocationHandler的invoke回调(具体原因,需要读源码)
                        //对于动态代理需要现有上面的认识,源码部分可以暂时放下(说难也不难,花点时间便可以了)

                        //这里的参数1和参数2是传递activity和activity中的method(根据业务场景决定传递的参数)
                        ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context, method);
                        Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler);
                        //此时proxy就是动态代理类的实例,代理的对象通过参数可以知道,为listenerType(即new View.OnClickListener或者View.OnLongClickListener)

                        //代理对象的方法(new View.OnClickListener的onClick和View.OnLongClickListener的OnLongClick,后面只写一个)的执行都会经过InvocationHandler的invoke回调

                        //那么现在我们该如何实现下一步呢?
                        //我们提及过,本质上还是要对相关View设置setOnClickListener监听,这一点通过反射就可以做到,如下
                        Method onClickMethod = view.getClass().getMethod(listenerSetter, listenerType);
                        //调用view的listenerSetter方法,参数是proxy
                        onClickMethod.invoke(view, proxy);

                        //经过以上步骤,可以实现,当用户点击了View就会触发View的setOnClickListener监听中的onClick回调,根据动态代理可以知道,会触发ListenerInvocationHandler的invoke方法
                        //而在创建ListenerInvocationHandler对象的时候传递了activity和activity中的method,如此,就实现了回调activity中的方法了。
                    }

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

七.问题收集

  • 仅作记录,不是本文的重点,可以不用看。仅想在此记录一下。(Retrofit单独抽取一篇文章做总结)

  • Retrofit采用的是OKHttp为啥能直接将返回的数据用于渲染UI?

    • Retrofit通过采用构建者模式,在执行build的时候创建了一个主线程(MainThreadExecutor),在调用enqueue方法的时候利用传递过来的MainThreadExecutor切换了主线程[即具体切换主线程的时机]。enqueue方法内部当有响应数据的时候,主线程就执行了一个excute方法。
    • 在网络请求返回响应的数据时进行了线程的切换;
  • Retrofit一些关键的技术点

    • 注解
    • 构建者模式
    • 动态代理(最重要的一种设计模式)
    • 线程切换
    • Okhttp
  • retrofit的核心原理(动态代理 + 注解)

    • 1.通过构建者模式创建Retrofit;
    • 2.通过create方法(内部是动态代理实现)
      • 把我们所定义的接口转化成接口实例
      • 并使用接口中的方法
    • 3.最终的网络请求是基于okhttp的进一步封装实现;
0

评论区