android apt示例

一、简介

APT( annotation processing tool )即注解处理工具。用于处理编译时注解,在编译期生成 .java文件,可以方便我们减少一些重复代码的编写。像ButterKnife、Dagger、EventBus、AndroidAnnotation等常用框架都用到了apt。下面我们通过简单的例子实现类似ButterKnife的bindView、bindOnClick功能。

二、实现流程

先看一下项目整体结构:

在这里插入图片描述
项目中包含了appmyannotationprocessor三个module。其中myannotation是我们的自定义注解,processor为注解处理器。为什么要写两个module呢?因为apt需要继承AbstractProcessor类,该类需要在new module–>Java Library里面才能获取到并且编译时注解处理器仅在编译时作用,不会参与apk打包,所以processor独立成一个module。而自定义注解需同时提供给app和processor使用,所以myannotation也独立成一个module。

1. 自定义注解

新建一个Java Library:myannotation,创建两个自定义注解类,

/***
 * 自定义BindView注解
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

/**
 * 自定义onclick注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int value();
}

以上定义了BindView、OnClick两个注解,BindView作用对象为成员变量(FIELD)、OnClick作用对象为方法(METHOD),两者的生命周期都是编译时(CLASS)。

注:这两个类里面是不应有中文字符的,我这里为了方便添加了中文注释。在编译时会出现编码GBK的不可映射字符的错误,或者添加以下代码到build.gradle文件下解决该问题:

tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

关于注解详细解析可以看这里

2. 实现注解处理器

新建一个Java Library:processor。依赖myannotation。

2.1 添加MyProcessor类继承AbstractProcessor:

public class MyProcessor extends AbstractProcessor {
    private Messager messager;
    private Elements elementUtils;
    private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //获取message、elements
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //定义支持的注解类型为 自定义的BindView、OnClick
        HashSet<String> supportedType = new LinkedHashSet<>();
        supportedType.add(BindView.class.getCanonicalName());
        supportedType.add(OnClick.class.getCanonicalName());
        return supportedType;
    }

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

通常我们需要实现四个方法:

  • init():初始化,获取ElementUtils以及Messager,ElementUtils在我们创建代码时用于获取包名等信息,Messager用于编译时打印相关信息。
  • getSupportedAnnotationTypes():返回支持注解的列表,这里添加了我们已经定义好的注解BindView和OnClick。
  • getSupportedSourceVersion():指定使用的Java版本,通常这里返回最新支持版本。
  • process():关键方法,用于处理注解以及生成相关的java代码。

下面来看process()方法的完整代码:

  @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //输出打印信息
        messager.printMessage(Diagnostic.Kind.NOTE, "processing...");
        mProxyMap.clear();
        //获取所有被BindView注解修饰的元素
        Set<? extends Element> elements = roundEnv.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(elementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id, variableElement);
        }
        //获取所有被OnClick注解修饰的元素
        Set<? extends Element> onClickElement = roundEnv.getElementsAnnotatedWith(OnClick.class);
        for (Element element :
                onClickElement) {

            TypeElement classElement = (TypeElement) element.getEnclosingElement();
            ExecutableElement executableElement = (ExecutableElement) element;
            String fullClassName = classElement.getQualifiedName().toString();
            ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new ClassCreatorProxy(elementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            OnClick bindAnnotation = element.getAnnotation(OnClick.class);
            int id = bindAnnotation.value();
            proxy.putOnclickElement(id, executableElement);
        }
//        //通过遍历mProxyMap,创建java文件
        for (String key : mProxyMap.keySet()) {
            ClassCreatorProxy proxyInfo = mProxyMap.get(key);
            try {
                messager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName());
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                messager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName() + "error");
            }
        }
        messager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true;
    }

Map<String, ClassCreatorProxy> mProxyMap是保存包含了自定义注解的类的集合,String参数以类的全类名作为标识,ClassCreatorProxy则是用来生成代码的代理类。首先获取到被注解修饰的元素,再从该元素获取全类名,如果该类已经存在于mProxyMap里面,则取出ClassCreatorProxy。否则将全类名和新建的ClassCreatorProxy保存到mProxyMap中。最后将注解信息保存到ClassCreatorProxy类中。

 这里有两段逻辑一样的代码分别处理了BindView和OnClick注解。

紧接着遍历mProxyMap,通过createSourceFile方法创建了java文件,ClassCreatorProxy的generateJavaCode方法向文件里面编写代码。

以上就是MyProcessor的基本工作了,接下来看生成代码的类ClassCreatorProxy。

2.2 代码的生成:ClassCreatorProxy

这里是用字符串拼接的方法来实现,逻辑比较简单,直接贴代码了:

public class ClassCreatorProxy {

    private Elements elements;
    private TypeElement typeElement;
    private String mPakageName;
    private String mBindingClassName;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap();
    private Map<Integer, ExecutableElement> onClickMap = new HashMap();

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

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

    public void putOnclickElement(int id, ExecutableElement element) {
        onClickMap.put(id, element);
    }

    //创建java代码
    public String generateJavaCode() {
        //StringBuilder 速度大于StringBuffer ,线程不安全
        StringBuilder sb = new StringBuilder();
        sb.append("package ").append(mPakageName).append(";\n\n");
        //sb.append("import com.jia.reflectionandannotation.*;\n");
        sb.append("\n");
        sb.append("public class ").append(mBindingClassName).append(" {\n");
        generateBindViewMethod(sb);
        generateBindOnclickMethod(sb);
       // generateOnclickMethod2(sb);
        sb.append(" \n}");
        return sb.toString();
    }

    //添加bindView方法
    public void generateBindViewMethod(StringBuilder sb) {

        sb.append("\tpublic void bindView(" + typeElement.getQualifiedName() + " activity){\n");
        for (int id :
                mVariableElementMap.keySet()) {
            VariableElement variableElement = mVariableElementMap.get(id);
             //获取view的类型:TextView、Button。。。
            String viewType = variableElement.asType().toString();
            //获取view的名字
            String viewName = variableElement.getSimpleName().toString();
            //生成 activity.textView=(TextView)activity.findViewById(id);
            sb.append("\t\tactivity." + viewName + " = (" + viewType + ")activity.findViewById(" + id + ");\n");
        }
        sb.append("\t}");
    }

    private void generateBindOnclickMethod(StringBuilder sb) {
        sb.append("\n\tpublic void bindOnClick(final " + typeElement.getQualifiedName() + " activity){\n");
        for (int id :
                onClickMap.keySet()) {
            ExecutableElement executableElement = onClickMap.get(id);
            String methodName = executableElement.getSimpleName().toString();//方法的名字
            sb.append("\t\tactivity.findViewById(" + id + ").setOnClickListener(new android.view.View.OnClickListener() {\n" +
                    "\t\t\t@Override\n" +
                    "\t\t\tpublic void onClick(android.view.View view) {\n" +
                    "\t\t\t\tactivity." + methodName + "();\n" +
                    "\t\t\t}\n" +
                    "\t\t});\n");
        }
        sb.append("\t}");
    }
    public String getProxyClassFullName() {
        return mPakageName + "." + mBindingClassName;
    }

    public TypeElement getTypeElement() {
        return typeElement;
    }

}

分别用了mVariableElementMap、onClickMap保存BindView注解和OnClick注解。注意两者的元素类型为VariableElement、ExecutableElement分别对应修饰FIELD和METHOD的注解,具体Elments解析可以参考这里
putElement()用于保存BindView注解;
putOnclickElement()用于保存OnClick注解;
之后使用generateBindViewMethod()方法遍历BindView注解,获取注解的值(id)、被注解的变量名,拼接生成findviewbyid代码。OnClick注解同理,最终会在 xxx_ViewBinding类中生成bindView()和bindOnClick()方法。

*或者用javapoet来生成代码,这里就不作介绍了。

2.3 添加auto-service依赖

 implementation 'com.google.auto.service:auto-service:1.0-rc2'
    //gradle3.4.1以上必须多加这一句,否则不会执行apt注解器
 annotationProcessor 'com.google.auto.service:auto-service:1.0-rc2'

最后MyProcessor前面添加注解,表示使用该类处理注解:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
....
注意!别忘了加annotationProcessor这一句!gradle3.1.2以下不需要加,高版本必须加,否则就算加了AutoService
也不会处理注解!我这里用的是3.4.1,具体看自己版本。

三、生成代码

在app build.gradle下添加依赖:

   annotationProcessor project(path: ':processor')
   implementation project(path: ':myannotation')
注意:processor module使用的是annotationProcessor,否则编译时会报错。

在MainActivity中使用注解,是不是跟butterknife差不多呢,哈哈:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.text1)
    TextView text1;
    
    @OnClick(R.id.button)
    void onButtonClick() {
        Toast.makeText(this, "onButtonClick", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindViewUtils.bindView(this);
        text1.setText("this is apt sample");
    }
}

添加完注解之后rebuildProject可以看到generatedJava->com.example.xxx下面生成了相应的MainActivity_ViewBinding类:
在这里插入图片描述

四、调用生成的代码

可以看到上面有个BindViewUtils.bindView(this)方法,实际上只是简单的反射调用

public class BindViewUtils {
    public static void bindView(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class<?> bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method bindView = bindViewClass.getMethod("bindView", activity.getClass());
            bindView.invoke(bindViewClass.newInstance(),activity);
            Method bindOnClick = bindViewClass.getMethod("bindOnClick", activity.getClass());
            bindOnClick.invoke(bindViewClass.newInstance(),activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

反射获得MainActivity_ViewBinding类,并调用了里面的bindView(),bindOnClick()方法。

五、结果

在这里插入图片描述
原本的helloworld变成了this is apt sample,按钮点击也弹出了Toast onButtonClick。另外butterknife的@OnCheckedChanged、@OnPageChange、@OnItemClick等原理都差不多,就不一一实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值