APT技术

作者: Chase_stars | 来源:发表于2019-09-14 10:20 被阅读0次

    社会犹如一条船,每个人都要有掌舵的准备。 — 易卜生

    写在前面

    在上一篇文章《注解入门》最后提出了一个问题编译时注解是什么?

    注解的生命周期为RUNTIME称为运行时注解,注解的生命周期为CLASS就称为编译时注解。

    APT(Annotation Processing Tool)技术就是通过编译期解析注解,并且生成Java代码的一种技术,一般会结合Javapoet技术生成Java代码。

    前期准备

    在正式讲解APT技术之前,先认识两个知识点,分别是Element和SPI机制,这是APT技术的基础,一定要掌握。

    1.Element

    Element指的是一系列与之相关的接口集合,存在javax.lang.model.element包下。Element代表的是程序的一个元素,可以是包,类,接口,属性变量,方法,方法形参,泛型参数等元素。Element所代表的元素只在编译器可见,用于保存元素在编译期间的各种状态。

    Element.png
    2.SPI机制

    SPI(Service Provider Interface)的作用是为接口寻找服务实现,在项目中通过配置文件为接口寻找服务实现。

    下面就介绍如何配置文件:

    SPI配置文件.png
    • 首先在main目录下创建resources目录。
    • 然后在resources目录下创建META-INF目录。
    • 接下来在META-INF目录下创建services目录。
    • 最后创建一个文件名为javax.annotaion.processing.Processor的文件,其中的内容就是为接口寻找服务实现的全类名。

    正式讲解

    现在通过一个例子介绍APT技术,在Android应用开发中,ButterKnife是一个很好用的第三方框架,不知道的同学请自行百度,这里就通过写一个简单的ButterKnife实现自动绑定View的框架。

    1.创建注解

    新建一个Java Library Module,名称为apt_annotation,并创建一个注解BindView。

    @Documented
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
    
        int viewId() default 0;
    }
    
    2.创建注解处理器

    新建一个Java Library Module,名称为apt_processor,并创建一个注解处理器AptProcessor,用来生成java代码。

    public class AptProcessor extends AbstractProcessor {
    
        /**
         * 初始化函数
         *
         * @param processingEnv
         */
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
        }
    
        /**
         * 指定Java版本号,一般返回最新版本号
         * 可以使用@SupportedSourceVersion注解代替
         *
         * @return
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        /**
         * 指定可解析的注解类型
         * 可以使用@SupportedAnnotationTypes注解代替
         *
         * @return
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> annotationTypes = new LinkedHashSet<>();
            annotationTypes.add(BindView.class.getCanonicalName());
            return annotationTypes;
        }
    
        /**
         * 核心函数
         *
         * @param annotations
         * @param roundEnv
         * @return
         */
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            Map<String, Map<String, Set<Element>>> elementMap = parseElements(roundEnv.getElementsAnnotatedWith(BindView.class));
            generateJavaFile(elementMap);
            return true;
        }
    
        /**
         * 解析全部元素
         *
         * @param elements
         * @return
         */
        private Map<String, Map<String, Set<Element>>> parseElements(Set<? extends Element> elements) {
            Map<String, Map<String, Set<Element>>> elementMap = new LinkedHashMap<>();
            // 遍历全部元素
            for (Element element : elements) {
                // 判断当前元素是否是属性变量
                if (!element.getKind().isField()) {
                    continue;
                }
    
                // 获取属性变量的上一级元素,即类元素
                TypeElement typeElement = (TypeElement) element.getEnclosingElement();
                // 获取类名
                String typeName = typeElement.getSimpleName().toString();
                // 获取类的上一级元素,即包元素
                PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();
                // 获取包名
                String packageName = packageElement.getQualifiedName().toString();
                
                Map<String, Set<Element>> typeElementMap = elementMap.get(packageName);
                if (typeElementMap == null) {
                    typeElementMap = new LinkedHashMap<>();
                }
    
                Set<Element> variableElements = typeElementMap.get(typeName);
                if (variableElements == null) {
                    variableElements = new LinkedHashSet<>();
                }
                variableElements.add(element);
    
                typeElementMap.put(typeName, variableElements);
                elementMap.put(packageName, typeElementMap);
            }
    
            return elementMap;
        }
    
        /**
         * 生成Java文件
         *
         * @param elementMap
         */
        private void generateJavaFile(Map<String, Map<String, Set<Element>>> elementMap) {
            Set<Map.Entry<String, Map<String, Set<Element>>>> packageElements = elementMap.entrySet();
            for (Map.Entry<String, Map<String, Set<Element>>> packageEntry : packageElements) {
    
                String packageName = packageEntry.getKey();
                Map<String, Set<Element>> typeElementMap = packageEntry.getValue();
    
                Set<Map.Entry<String, Set<Element>>> typeElements = typeElementMap.entrySet();
                for (Map.Entry<String, Set<Element>> typeEntry : typeElements) {
    
                    String typeName = typeEntry.getKey();
                    Set<Element> variableElements = typeEntry.getValue();
    
                    ClassName className = ClassName.get(packageName, typeName);
    
                    FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(className, "target", Modifier.PRIVATE);
    
                    MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(className, "activity")
                            .addStatement("target = activity");
    
                    MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC);
    
                    for (Element element : variableElements) {
    
                        VariableElement variableElement = (VariableElement) element;
    
                        String variableName = variableElement.getSimpleName().toString();
                        BindView bindView = variableElement.getAnnotation(BindView.class);
    
                        bindMethodBuilder.addStatement("target." + variableName + " = activity.findViewById(" + bindView.viewId() + ")");
                        unbindMethodBuilder.addStatement("target." + variableName + " = null");
                    }
    
                    unbindMethodBuilder.addStatement("target = null");
    
                    TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(typeName + "_ViewBinding")
                            .addModifiers(Modifier.PUBLIC)
                            .addSuperinterface(ClassName.get("com.chad.apt.binder", "UnBinder"))
                            .addField(fieldSpecBuilder.build())
                            .addMethod(bindMethodBuilder.build())
                            .addMethod(unbindMethodBuilder.build());
    
                    JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();
    
                    try {
                        javaFile.writeTo(processingEnv.getFiler());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    由于BindView在apt_annotation中,并且通过Javapoet技术生成Java代码,所以需要在apt_processor的build.gradle中引入apt_annotation和javapoet库。如下图:

    apt_processor build.gradle.png
    3.创建SPI配置文件

    接下来在apt_processor中创建SPI配置文件,创建步骤在前面已经讲过,这里不再复述,只看javax.annotation.processing.Processor文件内容如何定义,如下图的文件内容为com.chad.apt.processor.AptProcessor,也就是为Processor接口寻找AptProcessor服务实现类。

    javax.annotation.processing.Processor.png
    4.创建绑定者

    新建一个Android Library Module,名称为apt_binder,并且创建一个绑定者AptBinder和一个UnBinder接口,AptBinder中的bind函数通过反射机制创建注解处理器生成的Java类并绑定View,UnBinder接口用来解绑View。

    public class AptBinder {
    
        public static UnBinder bind(Activity activity) {
            try {
                Class<?> activityClass = activity.getClass();
                Class<?> viewBindingClass = Class.forName(activityClass.getName() + "_ViewBinding");
                Method bindMethod = viewBindingClass.getMethod("bind", activityClass);
                UnBinder unBinder = (UnBinder) viewBindingClass.newInstance();
                bindMethod.invoke(unBinder, activity);
                return unBinder;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    public interface UnBinder {
    
        void unbind();
    }
    
    5.如何使用

    新建一个Android Module,名称为app,并且将apt_annotation,apt_processor和apt_binder三个Library引入到app build.gradle。如下图:

    app build.gradle.png
    • 创建activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/id_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50dp"
            app:layout_constraintBottom_toBottomOf="@+id/id_world"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <TextView
            android:id="@+id/id_world"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="50dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/id_hello" />
    
    </android.support.constraint.ConstraintLayout>
    
    • 创建MainActivity
    public class MainActivity extends AppCompatActivity {
    
        private static final String TAG = MainActivity.class.getSimpleName();
    
        @BindView(viewId = R.id.id_hello)
        TextView mTvHello;
        @BindView(viewId = R.id.id_world)
        TextView mTvWorld;
    
        private UnBinder unBinder;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // 绑定
            unBinder = AptBinder.bind(this);
    
            mTvHello.setText("Hello!!!");
            mTvWorld.setText("World!!!");
            Log.d(TAG, "bind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
    
            // 解绑
            unBinder.unbind();
            Log.d(TAG, "unbind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
        }
    }
    

    在MainActivity中通过注解声明TextView属性变量,在onCreate()函数中通过AptBinder.bind()实现自动绑定View并返回一个UnBinder,UnBinder调用unbind()函数用来解绑View。

    然后rebuild就会自动生成java代码,即build > generated > source > apt > debug/release > com.chad.annotation目录下的MainActivity_ViewBinding.java文件。

    public class MainActivity_ViewBinding implements UnBinder {
      private MainActivity target;
    
      public void bind(MainActivity activity) {
        target = activity;
        target.mTvHello = activity.findViewById(2131165252);
        target.mTvWorld = activity.findViewById(2131165253);
      }
    
      @Override
      public void unbind() {
        target.mTvHello = null;
        target.mTvWorld = null;
        target = null;
      }
    }
    
    6.工程目录

    下图就是整个工程的目录结构:

    工程目录.png

    总结

    APT技术可以在编译期间生成Java代码,对于注解而言,有效的避免了运行时注解通过反射解析注解信息影响效率的问题。

    SPI机制通过配置文件的方式使用起来很麻烦,即要创建目录又要创建文件。Google的auto-service库提供了一种简单的配置方式,通过注解就可以解决繁琐的创建过程,使用方式很简单,这里就不详细讲解了。

    项目地址:https://github.com/zhangjunxiang1995/Apt

    相关文章

      网友评论

        本文标题:APT技术

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