APT

前言

关于注解的基础知识,可以参考另一篇随笔——注解 ,这里不再复述。

注解的保留时间分为三种:

  • SOURCE——只在源代码中保留,编译器将代码编译成字节码文件后就会丢掉
  • CLASS——保留到字节码文件中,但Java虚拟机将class文件加载到内存是不一定在内存中保留
  • RUNTIME——一直保留到运行时

通常我们使用后两种,因为SOURCE主要起到标记方便理解的作用,无法对代码逻辑提供有效的信息。

 时间解析性能影响
RUNTIME 运行时 反射
CLASS 编译期 APT+JavaPoet

如上图,对比两种解析方式:

  • 运行时注解比较简单易懂,可以运用反射技术在程序运行时获取指定的注解信息,因为用到反射,所以性能会收到一定影响。
  • 编译期注解可以使用APT(Annotation Processing Tool)技术,在编译期扫描和解析注解,并结合JavaPoet技术生成新的java文件,是一种更优雅的解析注解的方式,不会对程序性能产生太大影响。

下面以BindView为例,介绍两种方式的不同使用方法。


运行时注解

运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情。

首先,新建一个注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindViewTo {
    int value() default -1; //需要绑定的view id
}

然后,新建一个注解解析工具类AnnotationTools,和一般的反射用法并无不同:

public class AnnotationTools {

    public static void bindAllAnnotationView(Activity activity) {
        //获得成员变量
        Field[] fields = activity.getClass().getDeclaredFields();

        for (Field field : fields) {
            try {
                if (field.getAnnotations() != null) {
                    //判断BindViewTo注解是否存在
                    if (field.isAnnotationPresent(BindViewTo.class)) {
                        //获取访问权限
                        field.setAccessible(true);
                        BindViewTo getViewTo = field.getAnnotation(BindViewTo.class);
                        //获取View id
                        int id = getViewTo.value();
                        //通过id获取View,并赋值该成员变量
                        field.set(activity, activity.findViewById(id));
                    }
                }
            } catch (Exception e) {
            }
        }
    }
}

在Activity中调用

public class MainActivity extends AppCompatActivity {

    @BindViewTo(R.id.text)
    private TextView mText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //调用注解绑定,当前Activity中所有使用@BindViewTo注解的控件将自动绑定
        AnnotationTools.bindAllAnnotationView(this);

        //测试绑定是否成功
        mText.setTextColor(Color.RED);
    }

}

测试结果毫无意外,字体变成了红色,说明绑定成功。


编译期注解(APT+JavaPoet)

编译期注解解析需要用到APT(Annotation Processing Tool)技术,APT是javac中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,然后根据用户自定义的注解处理方法进行额外的处理。APT工具不仅能解析注解,还能结合JavaPoet技术根据注解生成新的的Java源文件,最终将生成的新文件与原来的Java文件共同编译。

APT实现流程如下:

  1. 创建一个java lib作为注解解析库——如apt_processor
  2. 在创建一个java lib作为注解声明库——如apt_annotation
  3. 搭建两个lib和主项目的依赖关系
  4. 实现AbstractProcessor
  5. 编译和调用

整个流程是固定的,我们的主要工作是继承AbstractProcessor,并且实现其中四个方法。下面一步一步详细介绍:

(1)创建解析库apt_processor

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.squareup:javapoet:1.9.0' // square开源的 Java 代码生成框架
    compile 'com.google.auto.service:auto-service:1.0-rc2' //Google开源的用于注册自定义注解处理器的工具
    implementation project(':apt_annotation') //依赖自定义注解声明库
}
sourceCompatibility = "7"
targetCompatibility = "7"

(2)创建注解库apt_annotation

声明一个注解BindViewTo,注意@Retention不再是RUNTIME,而是CLASS。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface BindViewTo {
    int value() default -1;
}

(3)搭建主项目依赖关系

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':apt_annotation')  //依赖自定义注解声明库
    annotationProcessor project(':apt_processor')  //依赖自定义注解解析库(仅编译期)
}

这里需要解释一下,因为注解解析库只在程序编译期有用,没必要打包进APK。所以依赖解析库使用的关键字是annotationProcessor,这是google为gradle插件添加的特性,表示只在编译期依赖,不会打包进最终APK。这也是为什么前面要把注解声明和注解解析拆分成两个库的原因。因为注解声明是一定要编译到最终APK的,而注解解析不需要。

 (4)实现AbstractProcessor

这是最复杂的一步,也是完成我们期望工作的重点。首先,我们在apt_processor中创建一个继承自AbstractProcessor的子类,重载其中四个方法:

  • init()——此处初始化一个工具类
  • getSupportedSourceVersion()——声明支持的Java版本,一般为最新版本
  • getSupportedAnnotationTypes()——声明支持的注解列表
  • process()——编译器回调方法,apt核心实现方法
具体代码如下:
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
//@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo")
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    private Elements mElementUtils;
    private HashMap<String, BinderClassCreator> mCreatorMap = new HashMap<>();

    /**
     * init方法一般用于初始化一些用到的工具类,主要有
     * processingEnvironment.getElementUtils(); 处理Element的工具类,用于获取程序的元素,例如包、类、方法。
     * processingEnvironment.getTypeUtils(); 处理TypeMirror的工具类,用于取类信息
     * processingEnvironment.getFiler(); 文件工具
     * processingEnvironment.getMessager(); 错误处理工具
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnv.getElementUtils();
    }

    /**
     * 获取Java版本,一般用最新版本
     * 也可以使用注解方式:@SupportedSourceVersion(SourceVersion.RELEASE_7)
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 获取目标注解列表
     * 也可以使用注解方式:@SupportedAnnotationTypes("com.xibeixue.apt_annotation.BindViewTo")
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindViewTo.class.getCanonicalName());
        return supportTypes;
    }

    /**
     * 编译期回调方法,apt核心实现方法
     * 包含所有使用目标注解的元素(Element)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //扫描整个工程, 找出所有使用BindViewTo注解的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindViewTo.class);
        //遍历元素, 为每一个类元素创建一个Creator
        for (Element element : elements) {
            //BindViewTo限定了只能属性使用, 这里强转为变量元素VariableElement
            VariableElement variableElement = (VariableElement) element;
            //获取封装属性元素的类元素TypeElement
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //获取简单类名
            String fullClassName = classElement.getQualifiedName().toString();
            BinderClassCreator creator = mCreatorMap.get(fullClassName);
            //如果不存在, 则创建一个对应的Creator
            if (creator == null) {
                creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement), classElement);
                mCreatorMap.put(fullClassName, creator);

            }
            //将需要绑定的变量和对应的view id存储到对应的Creator中
            BindViewTo bindAnnotation = variableElement.getAnnotation(BindViewTo.class);
            int id = bindAnnotation.value();
            creator.putElement(id, variableElement);
        }

        //每一个类将生成一个新的java文件,其中包含绑定代码
        for (String key : mCreatorMap.keySet()) {
            BinderClassCreator binderClassCreator = mCreatorMap.get(key);
            //通过javapoet构建生成Java类文件
            JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(),
                    binderClassCreator.generateJavaCode()).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return false;
    }
}

其中,BinderClassCreator是代码生成相关方法,具体代码如下:

public class BinderClassCreator {

    public static final String ParamName = "rootView";

    private TypeElement mTypeElement;
    private String mPackageName;
    private String mBinderClassName;
    private Map<Integer, VariableElement> mVariableElements = new HashMap<>();

    /**
     * @param packageElement 包元素
     * @param classElement   类元素
     */
    public BinderClassCreator(PackageElement packageElement, TypeElement classElement) {
        this.mTypeElement = classElement;
        mPackageName = packageElement.getQualifiedName().toString();
        mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding";
    }

    public void putElement(int id, VariableElement variableElement) {
        mVariableElements.put(id, variableElement);
    }

    public TypeSpec generateJavaCode() {
        return TypeSpec.classBuilder(mBinderClassName)
                //public 修饰类
                .addModifiers(Modifier.PUBLIC)
                //添加类的方法
                .addMethod(generateMethod())
                //构建Java类
                .build();

    }

    private MethodSpec generateMethod() {
        //获取全类名
        ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        //构建方法--方法名
        return MethodSpec.methodBuilder("bindView")
                //public方法
                .addModifiers(Modifier.PUBLIC)
                //返回void
                .returns(void.class)
                //方法传参(参数全类名,参数名)
                .addParameter(className, ParamName)
                //方法代码
                .addCode(generateMethodCode())
                .build();
    }

    private String generateMethodCode() {
        StringBuilder code = new StringBuilder();
        for (int id : mVariableElements.keySet()) {
            VariableElement variableElement = mVariableElements.get(id);
            //变量名称
            String name = variableElement.getSimpleName().toString();
            //变量类型
            String type = variableElement.asType().toString();
            //rootView.name = (type)view.findViewById(id), 注意原类中变量声明不能为private,否则这里是获取不到的
            String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName + ".findViewById(" + id + ");
";
            code.append(findViewCode);

        }
        return code.toString();
    }

    public String getPackageName() {
        return mPackageName;
    }
}

(5)编译和调用

在MainActivity中调用,这里需要强调的是待绑定变量不能声明为private,原因在上面代码注释中已经解释了。

public class MainActivity extends AppCompatActivity {

    @BindViewTo(R.id.text)
    public TextView mText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);//这里的MainActivity需要先编译生成后才能调用
        new MainActivity_ViewBinding().bindView(this);
        //测试绑定是否成功
        mText.setTextColor(Color.RED);
    }
}

此时,build或rebuild工程(需要先注掉MainActivity的调用),会看到在generatedJava文件夹下生成了新的Java文件。

上面的调用方式需要先编译一次才能使用,当有多个Activity时比较繁琐,而且无法做到统一。

我们也可以选择另一种更简便的方法,即反射调用。新建工具类如下:

public class MyButterKnife {

        public static void bind(Activity activity) {
            Class clazz = activity.getClass();
            try {
                Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
                Method method = bindViewClass.getMethod("bindView", activity.getClass());
                method.invoke(bindViewClass.newInstance(), activity);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

}

调用方式改为:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //通过反射调用
        MyButterKnife.bind(this);

        //测试绑定是否成功
        mText.setTextColor(Color.RED);
    }

此方式虽然也会稍微影响性能,但依然比直接使用运行时注解高效得多。


总结

说到底,APT是一个编译器工具,是一个非常好的从源码到编译期的过渡解析工具。虽然结合JavaPoet技术被各大框架使用,但是依然存在固有的缺陷,比如变量不能私有,依然要采用反射调用等,普通开发者可斟酌使用。

个人认为APT有如下优点:

  1. 配置方式,替换文件配置方式,改为代码内配置,提高程序内聚性
  2. 代码精简,一劳永逸,省去繁琐复杂的格式化代码,适合团队内推广

以上优点同时也是缺点,因为很多代码都在后台生成,会对新同学造成理解困难,影响其对整体架构的理解,增加学习成本。

近期研究热修复和APT,发现从我们写完成代码,到代码真正执行,期间还真是有大把的“空子”可以钻啊,借图mark一下。

原文地址:https://www.cnblogs.com/not2/p/11492806.html