APT技术

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

写在前面

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

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

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

前期准备

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

1.Element

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

APT技术_第1张图片
Element.png
2.SPI机制

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

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

APT技术_第2张图片
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 getSupportedAnnotationTypes() {
        Set annotationTypes = new LinkedHashSet<>();
        annotationTypes.add(BindView.class.getCanonicalName());
        return annotationTypes;
    }

    /**
     * 核心函数
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        Map>> elementMap = parseElements(roundEnv.getElementsAnnotatedWith(BindView.class));
        generateJavaFile(elementMap);
        return true;
    }

    /**
     * 解析全部元素
     *
     * @param elements
     * @return
     */
    private Map>> parseElements(Set elements) {
        Map>> 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> typeElementMap = elementMap.get(packageName);
            if (typeElementMap == null) {
                typeElementMap = new LinkedHashMap<>();
            }

            Set 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>> elementMap) {
        Set>>> packageElements = elementMap.entrySet();
        for (Map.Entry>> packageEntry : packageElements) {

            String packageName = packageEntry.getKey();
            Map> typeElementMap = packageEntry.getValue();

            Set>> typeElements = typeElementMap.entrySet();
            for (Map.Entry> typeEntry : typeElements) {

                String typeName = typeEntry.getKey();
                Set 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技术_第3张图片
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。如下图:

APT技术_第4张图片
app build.gradle.png
  • 创建activity_main.xml



    

    


  • 创建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.工程目录

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

APT技术_第5张图片
工程目录.png

总结

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

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

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

你可能感兴趣的:(APT技术)