Android基础系列(3)编译时注解处理器APT

793 阅读2分钟

1 简述

  APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,然后用Annotation处理器进行处理。
  Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件,APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
  简单的说:不影响性能的情况下,自动生成代码
  常用的ButterKnife、Dagger等工具都是用了APT技术。

2 示例

我们通过实现类似ButterKnife自动生成findviewById功能,来深入体会APT技术的魅力。
APT工作流程

  1. 定义注解
  2. 定义注解处理器(AbstractProcess)
  3. 处理器中生成java代码(可以采用javaPoet)
  4. 注册处理器(AutoService)

企业微信截图_73470ef0-9563-41f4-85d1-b75fad968720.png

创建apt-annotation模块
定义BindView注解

@Retention(RetentionPolicy.CLASS) // 编译时注解
@Target(ElementType.FIELD) // 注解范围为类成员
public @interface BindView {
    int value();
}

创建apt-processor模块
定义注解处理器,生成需要的代码,注册处理器

// BindViewProcessor.java
@AutoService(Processor.class) // 注册处理器
public class BindViewProcessor extends AbstractProcessor {

    private Elements mElementUtils;
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();

    // ProcessingEnvironment提供很多有用的工具类Elements, Types 和 Filer
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
    }

    /**
     * 指定这个注解处理器是注册给哪个注解的,这里是 BindView
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //根据注解生成Java文件
        mProxyMap.clear();
        //得到所有的注解
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new ClassCreatorProxy(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id, variableElement);
        }
        for (String key : mProxyMap.keySet()) {
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode()).build();
            try {
                // 生成文件
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }
}


// ClassCreatorProxy.java
// JavaPoet生成java代码
public class ClassCreatorProxy {
    private String mBindingClassName;
    private String mPackageName;
    private TypeElement mTypeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();

    public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
        this.mTypeElement = classElement;
        PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
        String packageName = packageElement.getQualifiedName().toString();
        String className = mTypeElement.getSimpleName().toString();
        this.mPackageName = packageName;
        this.mBindingClassName = className + "_ViewBinding";
    }

    public void putElement(int id, VariableElement element) {
        mVariableElementMap.put(id, element);
    }

    /**
     * 创建Java代码
     */
    public TypeSpec generateJavaCode() {
        return TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethods())
                .build();

    }

    /**
     * 加入Method
     */
    private MethodSpec generateMethods() {
        ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(host, "host");

        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
        }
        return methodBuilder.build();
    }

    public String getPackageName() {
        return mPackageName;
    }
}

创建library(apt-library)
反射执行bind方法

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

在app主模块中导入apt相关的三个包,使用BindView注解

public class TestActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView mTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        BindViewTools.bind(this);
        mTextView.setText("hello world");
    }
}

编译结果

企业微信截图_3230a302-b760-444e-9fcf-7d5c8ab51264.png
可以在/build/generated/ap_generated_sources下找到生成的Java文件。

写在最后

通过上面这个使用APT生成代码的简单例子,想必大家对APT都有了一个直观的理解。
如市面上非常火的ButterKnife框架,里面就用到了APT技术,可以去研究研究。
这种技术配上javapoet框架,是可以写出很多“高大上”的框架。