利用APT实现Android编译时注解

一、APT概述

我们在前面的java注解详解一文中已经讲过,可以在运行时利用反射机制运行处理注解。其实,我们还可以在编译时处理注解,这就是不得不说官方为我们提供的注解处理工具APT (Annotation
Processing Tool )。

APT用来在编译时期扫描处理源代码中的注解信息,我们可以根据注解信息生成一些文件,比如Java文件。利用APT为我们生成的Java代码,实现冗余的代码功能,这样就减少手动的代码输入,提升了编码效率,而且使源代码看起来更清晰简洁。

从Java5开始,JDK就自带了注解处理器APT,不过从近几年开始APT才真正的流行起来,这要得益于Android上各种主流库都用了APT来实现,比如Dagger、ButterKnife、AndroidAnnotation、EventBus等。因为我本身工作中经常用到上面这些框架,为了更深入了解这些框架的实现过程,因此想利用APT技术实现自己的编译时注解。

二、实现目标

在Android开发中我们经常要编写如下冗余的代码:

Button button = (Button) findViewById(R.id.button1);
    Button button2 = (Button) findViewById(R.id.button2);

    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {}
        });

    button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {}
    });

编写上面这些冗余代码不但浪费时间,而且一定程度上造成源代码冗余复杂。那么为了解决这种问题,我们可以利用注解和APT工具生成一个代理类(ProxyClass),让这个代理类帮助我们实现上面这些冗余的代码。

首先我们通过自定义的注解注解要处理的元素:

public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.tv)
    TextView textView;
    @ViewById(R.id.btn)
    Button button;

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

    @Click({R.id.tv, R.id.btn})
    public void myClick(View view) {
        //Click
    }

然后我们利用APT生成MainActivity的代理类 MainActivity$$Proxy,让该类帮我们实现冗余的功能:

public class MainActivity$$Proxy implements IProxy<MainActivity> {
  @Override
  public void inject(final MainActivity target, View root) {

    target.button = (Button)(root.findViewById(R.id.btn));
    target.textView = (TextView)(root.findViewById(R.id.tv));

    View.OnClickListener listener;

    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        target.myClick(view);
      }
    } ;

    (root.findViewById(R.id.btn)).setOnClickListener(listener);
    (root.findViewById(R.id.tv)).setOnClickListener(listener);
  }
}

OK,我们接下来就要实现上面的功能。

三、 项目框架

我把我们的注解框架命名为ProxyTool,把该框架分为四个模块,前三个为核心模块:

  • proxytool-api:框架api模块,供使用者调用,Android Library类型模块
  • proxytool-annotations:自定义注解模块,Java类型模块
  • proxytool-compiler:注解处理器模块,用于处理注解并生成文件,Java类型模块
  • proxytool-sample:示例Demo模块,Android工程类型模块

其中这四个模块的依赖关系如下:

proxytool-api依赖proxytool-annotations模块。 
proxytool-compiler依赖proxytool-annotations模块。 
proxytool-sample模块依赖proxytool-api模块。

有人也许会问,为什么不可以将这写模块写在一起呢?

因为注解处理模块器proxytool-compiler只在我们编译过程中需要使用到,在APP运行阶段就不需要使用该模块了。所以在发布APP时,我们就不必把注解处理器模块打包进来,以免造成程序臃肿,所以把proxytool-compiler模块单独拿出来。同时注解处理模块和api模块都需要使用到自定义注解模块,所以就需要把自定义注解模块单独拿出来。这样为何需要分成三个模块的原因也就一目了然了,其实butterfnife框架也是这样分的。

四、自定义注解模块

首先我们在自定义注解模块中定义两个注解类型,分别用于绑定View的id和注册View的点击事件:

/**
 *  Bind a field to the view for the specified ID
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface ViewById  {
    int value();
}

/**
 * Bind a method to an android.view.View.OnClickListener on the view for each ID specified.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnClick {
   public int[] value();
}

ViewById中的value用于接收注解该View的id值,OnClick中的value数组用于接收一组View的id值,这些view会被注册点击响应事件。因为这些注册只在编译时有需要用到,程序运行时就不再需要了,所以我们把这些注解定义成编译时保留(RetentionPolicy.SOURCE)即可。限于篇幅的考虑,下面的介绍中我只对ViewById注解做处理,OnClick注解处理也是类似的。

五、注解处理器模块

创建好自定义注解后,我们就需要利用java提供的注解处理器根据自定义的注解来生成代理类了,该模块需要依赖其他三个模块:

  compile 'com.squareup:javapoet:1.7.0'
  compile 'com.google.auto.service:auto-service:1.0-rc2'
  compile project(':proxytool-annotations')
  • javapoet是square公司出的一个帮助我们非常方便生成java代码文件的第三方库,避免我们手动拼接字符串的麻烦。
  • auto-service是google公司出的第三方库,主要用于注解处理器,可以自动帮我们生成META-INF 配置信息。
  • 注解处理器要根据自定义注解进行解析,所以也需要依赖该模块。

让我们看一下注解处理器的API。所有的注解处理器都必须继承AbstractProcessor,如下所示:

/**
 * 注解处理器
 */
@AutoService(Processor.class)
public class ProxyToolProcessor extends AbstractProcessor {

    private Filer mFiler; //文件相关工具类
    private Elements mElementUtils; //元素相关的工具类
    private Messager mMessager; //日志相关的工具类

    /**
     * 处理器的初始化方法,可以获取相关的工具类
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * 处理器的主方法,用于扫描处理注解,生成java文件
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
       ...

        return false;
    }

   /**
    *  指定哪些注解应该被注解处理器注册
    */
    @Override
    public Set<String> getSupportedOptions() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ViewById.class.getName());
        types.add(OnClick.class.getName());
        return  types;
    }

   /**
    *  用来指定你使用的 java 版本
    */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
  • @AutoService(Processor.class) 属于auto-service库,可以自动生成META-INF/services/javax.annotation.processing.Processor文件(该文件是所有注解处理器都必须定义的),免去了我们手动配置的麻烦。
  • init(ProcessingEnvironment processingEnvironment) 在处理器初始化的调用,通过processingEnv参数我们可以拿到一些实用的工具类ElementsMessagerFiler。我们在后面将会使用到它们。Elements,一个用来处理Element的工具类。Messager,一个用来输出日志信息的工具类。Filer、如这个类的名字所示,你可以使用这个类来创建文件。
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 这是注解处理器的主方法,你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。
  • getSupportedAnnotationTypes() 在这个方法里面你必须指定哪些注解应该被注解处理器注册。它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全限定名。
  • getSupportedSourceVersion() 用来指定你使用的 java 版本,通常我们返回SourceVersion.latestSupported()即可。
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({
        "proxytool.ViewById",
        "proxytool.OnClick"
})
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }
}

但是考虑到兼容性问题,建议还是重写getSupportedSourceVersion方法和getSupportedAnnotationTypes方法

Element元素

在继续讲解处理器之前,我们必须先明白Elment元素这个概念。在注解处理器中,我们扫描 java 源文件,源代码中的每一部分都是Element的一个特定类型。换句话说:Element代表程序中的元素,比如说
包,类,方法。在下面的例子中,我将添加注释来说明这个问题:

package com.example; //PackageElement

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}
  • ExecuteableElement:可以表示一个普通方法、构造方法、初始化方法(静态和实例)。
  • PackageElement:代表一个包名。
  • TypeElement:代表一个类、接口。
  • VariableElement:代表一个字段、枚举常量、方法或构造方法的参数、本地变量、或异常参数等。
  • Element:上述所有元素的父接口,代表源码中的每一个元素。

在注解处理器世界中,整个java代码被结构化了。我们需要像解析XML文件一样去解析整个源代码。Element就像XML解析器中的DOM元素,你可以通过如下两个方法获取该元素的子元素和父元素:

 Element getEnclosingElement();  //获取父元素
 List<? extends Element> getEnclosedElements(); //获取子元素

比如你有如下的一个类

public class Foo { 

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement
}

成员变量a通过getEnclosingElement方法返回的是的父元素是类Foo,类Foo通过getEnclosedElements返回的子元素就包括成员变量a、成员变量other以及构造方法Foo()。关于这两个方法更多的解释,可以参见官方文档

实现process方法

1. 收集注解信息

注解处理器中,最核心的方法就是process(),在这里你可以扫描和处理注解,并生成java文件。首先我们扫描所有被@ViewById注解的元素:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

   //处理被ViewById注解的元素
    for (Element element : roundEnv.getElementsAnnotatedWith(ViewById.class)) {
        if (!isValid(ViewById.class, "fields", element)) {
             return true;
        }
        parseViewById(element);
    }
  ...

roundEnvironment.getElementsAnnotatedWith(ViewById.class)方法返回一个被@ViewById注解的Element类型的元素列表。注意这里是Element列表,而不是类列表,Element可以包括类、方法、变量等。

2. 匹配准则

接下来我们需要对这个元素做进一步的检查,保证被注解的元素是符合规范的。如果使用者不按规范随意注解元素的话,程序是无法正常运行的。所以我们需要执行isVaid方法用于检测被注解元素的合法性:

private boolean isValid(Class<? extends Annotation> annotationClass, String targetThing, Element element) {
        boolean isVaild = true;

        //获取变量的所在的父元素,肯能是类、接口、枚举
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        //父元素的全限定名
        String qualifiedName = enclosingElement.getQualifiedName().toString();

        // 所在的类不能是private或static修饰
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
            error(element, "@%s %s must not be private or static. (%s.%s)",
                    annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            isVaild = false;
        }

        // 父元素必须是类,而不能是接口或枚举
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
                    annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            isVaild = false;
        }

        //不能在Android框架层注解
        if (qualifiedName.startsWith("android.")) {
            error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }
        //不能在java框架层注解
        if (qualifiedName.startsWith("java.")) {
            error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }

        return isVaild;
    }

    private void error(Element e, String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
    }

isValid方法中,它检查被注解的元素是否符合规则:

  • 被注解元素的父元素必须是个类,而不能是接口、枚举。
  • 被注解元素的父元素必须是非private 和 非static修饰。
  • 被注解的元素只能注解非框架层元素。

这里只是简单的列出几个是否符合注解规范的条件,更严格的判断条件还需要大家来完善。

3. 错误处理

不知道大家有没有发现,在error方法中利用了Messagerinit方法中获取到的工具类)来处理错误信息。Messager为注解处理器提供了一种报告错误消息,警告信息和其他消息的方式。它不是注解处理器开发者的日志工具。Messager是用来给那些使用了你的注解处理器的第三方开发者显示信息的。 
其中非常重要的是Kind.ERROR级别信息,因为这种消息类型是用来表明我们的注解处理器在处理过程中出错了。有可能是第三方开发者误使用了我们的@ViewById注解(比如,使用@ViewById注解了一个接口中的变量)。这个概念与传统的
java 应用程序有一点区别。传统的 java 应用程序出现了错误,你可以抛出一个异常。如果你在process()中抛出了一个异常,那 jvm 就会崩溃。注解处理器的使用者将会得到一个从 javac 给出的非常难懂的异常错误信息。因为它包含了注解处理器的堆栈信息。因此注解处理器提供了Messager类。它能打印漂亮的错误信息,而且你可以链接到引起这个错误的元素上。回到process中:

 @Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
   //处理被ViewById注解的元素
   for (Element element : roundEnvironment.getElementsAnnotatedWith(ViewById.class)) {
        if (!isValid(ViewById.class, "fields", element)) {
            return true;
        }
        parseViewById(element);
    }

    ...
}

为了能够获取Messager显示的信息,非常重要的是注解处理器必须不崩溃地完成运行。这就是我们在调用error()后,跳出isValid,执行return
true
的原因。如果我们在这里没有返回的话,process()就会继续运行,因为messager.printMessage(
Diagnostic.Kind.ERROR)
并不会终止进程。

4. 数据模型

一旦isValid方法检查通过,那么就表示该注解元素是可以使用的,因此我们继续执行parseViewById方法,把这些元素封装成model,供后面生成Java文件时使用。

    private Map<String, ProxyClass> mProxyClassMap = new HashMap<>();

    /**
     * 处理ViewById注解
     *
     * @param element
     */
    private void parseViewById(Element element) {
        ProxyClass proxyClass = getProxyClass(element);
        //把被注解的view对象封装成一个model,放入代理类的集合中
        FieldViewBinding bindView = new FieldViewBinding(element);
        proxyClass.add(bindView);
    }

    /**
     * 生成或获取注解元素所对应的ProxyClass类
     */
    private ProxyClass getProxyClass(Element element) {
        //被注解的变量所在的类
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        String qualifiedName = classElement.getQualifiedName().toString();
        ProxyClass proxyClass = mProxyClassMap.get(qualifiedName);
        if (proxyClass == null) {
            //生成每个宿主类所对应的代理类,后面用于生产java文件
            proxyClass = new ProxyClass(classElement, mElementUtils);
            mProxyClassMap.put(qualifiedName, proxyClass);
        }
        return proxyClass;
    }
  • parseViewById(Element element):在该方法中,首先需要通过getProxyClass方法获取一个ProxyClass类型的对象。ProxyClass代表了该注解元素所对应的类元素,这里我们利用面向对象的思想进行了封装。然后我们把被注解的元素也封装成一个FieldViewBinding类型的model,并放入到ProxyClass中。
  • getProxyClass(Element element):该方法主要是生成或获取注解元素所对应的类,。你可以在getProxyClass方法中看到,利用getEnclosingElement方法获取了该注解元素的父元素,也就是该注解元素的所在的类。我们把每一个类元素TypeElement都封装成了ProxyClass,并保存在HashMap中。

上面这两个方法的作用,主要是把注解的元素和注解元素所在的类都封装成了model,并存储起来,供我们后面生成Java源码时使用。下面表示了两个model类:

FieldViewBinding类:

public class FieldViewBinding {

    /** 注解元素*/
    private VariableElement mElement;

    /** 资源id*/
    private int mResId;

    /** 变量名*/
    private String mVariableName;

    /**变量类型*/
    private TypeMirror mTypeMirror;

    public FieldViewBinding(Element element) {

        mElement = (VariableElement) element;
        ViewById viewById = element.getAnnotation(ViewById.class);
        //资源id
        mResId = viewById.value();
        //变量名
        mVariableName = element.getSimpleName().toString();
        //变量类型
        mTypeMirror = element.asType();
    }

    public VariableElement getElement() { return mElement;}
    public int getResId() { return mResId;}
    public String getVariableName() {return mVariableName;}
    public TypeMirror getTypeMirror() {return mTypeMirror;}
}
public class ProxyClass {

    /**类元素 */
    public TypeElement mTypeElement;

    /**元素相关的辅助类*/
    private Elements mElementUtils;

    /** FieldViewBinding类型的集合*/
    private Set<FieldViewBinding> bindViews = new HashSet<>();

    public ProxyClass(TypeElement mTypeElement, Elements mElementUtils) {
        this.mTypeElement = mTypeElement;
        this.mElementUtils = mElementUtils;
    }

    public void add(FieldViewBinding bindView) {
        bindViews.add(bindView);
    }

    /**
     * 用于生成代理类
     */
    public JavaFile generateProxy() {
        ...
    }
}

ProxyClass类中的generateProxy()方法是用于生成每个类所对应的代理类,比如类MainActivity就会生成类MainActivity$$Proxy,该方法生成的详细过程会后面再讲。

文件的生成

既然我们已经收集到了注解元素和注解元素所在的类,那么我们就需要为每个类生成一个全新的代理类,在代理类中执行那些冗余的代码操作。继续回到process方法中:

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        ...

        //为每个宿主类生成所对应的代理类
        for (ProxyClass proxyClass_ : mProxyClassMap.values()) {
            try {
                proxyClass_.generateProxy().writeTo(mFiler);
            } catch (IOException e) {
                error(null, e.getMessage());
            }
        }
        mProxyClassMap.clear();

        return true;
    }

现在既然已经收集到了每个注解元素所对应的类,那么我们就需要为每个类生成所对应的代理类,遍历所有的类元素集合mProxyClassMap 通过ProxyClassgenerateProxy()方法来生成Java源码:

ProxyClass##generateProxy()

    //proxytool.IProxy
    public static final ClassName IPROXY = ClassName.get("proxytool.api", "IProxy");
    //android.view.View
    public static final ClassName VIEW = ClassName.get("android.view", "View");
    //生成代理类的后缀名
    public static final String SUFFIX = "$$Proxy";

    /**
     * 用于生成代理类
     */
    public JavaFile generateProxy() {

        //生成public void inject(final T target, View root)方法
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "target", Modifier.FINAL)
                .addParameter(VIEW, "root");

        //在inject方法中,添加我们的findViewById逻辑
        for (FieldViewBinding model : bindViews) {
            // find views
            injectMethodBuilder.addStatement("target.$N = ($T)(root.findViewById($L))", model.getVariableName(),
                    ClassName.get(model.getTypeMirror()), model.getResId());
        }

        // 添加以$$Proxy为后缀的类
        TypeSpec finderClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + SUFFIX)
                .addModifiers(Modifier.PUBLIC)
                //添加父接口
                .addSuperinterface(ParameterizedTypeName.get(IPROXY, TypeName.get(mTypeElement.asType())))
                //把inject方法添加到该类中
                .addMethod(injectMethodBuilder.build())
                .build();

        //添加包名
        String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString();

        //生成Java文件
        return JavaFile.builder(packageName, finderClass).build();
    }

上面我们使用了javapoet来帮助我们生成Java源码,免去手动拼接字符串的麻烦。生成过程很简单,看注释就明白了。

不知道大家发现了没有循环结束后我们还执行了mProxyClassMap.clear(),原因就在于process()可以被多次调用,因为新生成的Java文件很可能包括@ViewById注解,所以process方法会多次执行直到没有生成该注解为止。所以我们应该清空之前的数据,避免生成重复的代理类。

六、API模块

既然已经生成了代理类,那么我还需要提供API供使用者访问该代理类,供在Activity、Fragment、View中如下使用:

//Activity
ProxyTool.bind(this);

//Fragment
ProxyTool.bind(this, view);

//View
ProxyTool.bind(this);

ProxyToolbind方法中我们需要为需要为不同的目标(比如
Activity、Fragment 和 View 等)提供重载的注入方法,这些方法最终都调用createBinding方法:

public class ProxyTool {

   //Activity
    @UiThread
    public static void bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        createBinding(target, sourceView);
    }

   //View
    @UiThread
    public static void bind(@NonNull View target) {
        createBinding(target, target);
    }
    //Fragment
    @UiThread
    public static void bind(@NonNull Object target, @NonNull View source) {
        createBinding(target, source);
    }

    public static final String SUFFIX = "$$Proxy";

    public static void createBinding(@NonNull Object target, @NonNull View root) {

        try {
            //生成类名+后缀名的代理类,并执行注入操作
            Class<?> targetClass = target.getClass();
            Class<?> proxyClass = Class.forName(targetClass.getName() + SUFFIX);
            IProxy proxy = (IProxy) proxyClass.newInstance();
            proxy.inject(target, root);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
  • 我们重载了三个bind方法用于接收的不同目标(Activity、Fragment 和 View 等),target参数表示注解元素所在的类,root参数表示要查找的View的,因为Activity和View本身即是target也是root,所以只要一个参数即可,而Fragment中类和View是分离的,所以需要两个参数。
  • 所有bind方法最终调用了createBinding方法,在该方法中我们通过targetClass.getName()
    + SUFFIX
    ,拼接成代理类的全限定名,然后生成代理类的实例,并执行代理类的inject方法,该方法中执行的就是findViewById的操作。

这里我们还需要注意一点,所有生成的代理类都默认实现IProxy接口:

public interface IProxy<T> {
    /**

     * @param target 所在的类
     * @param root 查找 View 的地方
     */
    public void inject(final T target, View root);
}

该接口定义了inject方法,代理类中需要在该方法中实现具体的注入逻辑。代理类的生成和inject方法的实现,都是在注解处理器模块中进行处理,具体的生成过程都在文件的生成章节中,这里就不再讲。

七、项目中的使用

上面三个核心模块都已经介绍完了,现在让我们在具体的项目中使用吧。

首先在整个工程的build.gradle中添加如下:

    dependencies {
         ...
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }

然后在自己module的build.gradle中添加插件和依赖,如下所示:

apply plugin: 'com.neenbedankt.android-apt'

...

dependencies {
    ...
    compile project(':proxytool-api')
    apt project(':proxytool-compiler')
}

然后我们在项目中使用该框架的注解和API:

public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.btnOne)
    Button btnOne;
    @ViewById(R.id.btnTwo)
    Button btnTwo;

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

    @OnClick({R.id.btnOne,R.id.btnTwo})
    public void myClick(View view){
        btnOne.setText("111111");
        btnTwo.setText("222222");
    }
}

执行Make Project操作后,在build/generated/source/apt/debug/下就会生成所对应的代理类MainActivity$$Proxy

public class MainActivity$$Proxy implements IProxy<MainActivity> {
  @Override
  public void inject(final MainActivity target, View root) {
    target.btnTwo = (Button)(root.findViewById(2131427414));
    target.btnOne = (Button)(root.findViewById(2131427413));
    View.OnClickListener listener;
    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        target.myClick(view);
      }
    } ;
    (root.findViewById(2131427413)).setOnClickListener(listener);
    (root.findViewById(2131427414)).setOnClickListener(listener);
  }
}

八、总结

限于篇幅的考虑,上面只介绍了如何对ViewById注解进行处理,OnClick注解处理也是类似的。整个项目的地址:https://github.com/maofan4041/ProxyTool

参考: 
ANNOTATION PROCESSING 101 
Annotation-Processing-Tool详解 
Android 利用 APT 技术在编译期生成代码 
Android 如何编写基于编译时注解的项目 
万能的APT!编译时注解的妙用

</pre><pre class="prettyprint" name="code" style="white-space: nowrap; word-wrap: break-word; box-sizing: border-box; position: relative; overflow-y: hidden; overflow-x: auto; margin-top: 0px; margin-bottom: 1.1em; font-family: "Source Code Pro", monospace; padding: 5px 5px 5px 60px; font-size: 14px; line-height: 1.45; word-break: break-all; color: rgb(51, 51, 51); background-color: rgba(128, 128, 128, 0.0470588); border: 1px solid rgba(128, 128, 128, 0.0745098); border-radius: 0px;">
时间: 2024-10-07 19:40:45

利用APT实现Android编译时注解的相关文章

Android编译时注解框架系列2-Run示例

概述 先讲一下编写<Android编译时注解框架>的初衷吧,APT其实并不难,可以说是简单且高效,但关于 APT的资料却并不多,甚至很多人都不知道这么一个技术.国内关于APT的博客屈指可数,唯二找到的几篇初级讲解一个是用Eclipse写得,一个是用 AndroidStudio加Intellij.刚开始着实踩了不少坑,但事实是,APT完全可以用AndroidStudio单独实现.光是项目搭建就如此麻烦了,更别提语法讲解了.资料匮乏无疑提高了APT的入门门槛. 正因为如此,这个系列博客就这样诞生啦

Android 利用 APT 技术在编译期生成代码_Android

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入.现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐! (ง •̀_•́)ง 下面通过一个简单的 View 注入项目 ViewFinder 来介绍 APT 相关内容,简单实现了类似于ButterKnife 中的两种注解 @BindView 和 @OnClick . 项目

Android 利用 APT 技术在编译期生成代码

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入.现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐! (ง •̀_•́)ง 下面通过一个简单的 View 注入项目 ViewFinder 来介绍 APT 相关内容,简单实现了类似于ButterKnife 中的两种注解 @BindView 和 @OnClick . 项目

Android如何编写基于编译时注解的项目

一.概述 在Android应用开发中,我们常常为了提升开发效率会选择使用一些基于注解的框架,但是由于反射造成一定运行效率的损耗,所以我们会更青睐于编译时注解的框架,例如: butterknife免去我们编写View的初始化以及事件的注入的代码. EventBus3方便我们实现组建间通讯. fragmentargs轻松的为fragment添加参数信息,并提供创建方法. ParcelableGenerator可实现自动将任意对象转换为Parcelable类型,方便对象传输. 类似的库还有非常多,大多

实现一个简单的Java编译时注解处理器

简介 Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据.Java语言中的类.方法.变量.参数和包等都可以被标注.Java标注和Javadoc不同,标注有自反性.在编译器生成类文件时,标注可以被嵌入到字节码中,由Java虚拟机执行时获取到标注.根据元注解@Retention指定值的不同,注解可分为SOURCE.CLASS和RUNTIME三种类型.当被声明为SOURCE时,注解仅仅在源码级别被保留,编译时被丢弃:声明为CLASS时,注解会由编译器记录在clas

Java中利用final关键字inline编译优化真的有效吗?

(inkfish原创,转载请注明出处:http://blog.csdn.net/inkfish/) 为寻求java代码的性能优化,从网上搜到利用final关键字进行编译时inline优化的方法,但是真的有效吗?实际测试中发现未必,甚至性能影响巨大,最终放弃了使用final优化的想法. 测试环境:Windows XP SP2,JDK 1.6.0_15-b03,Eclipse 3.5 SR1.   package test; public class Test { public static voi

iOS Swift _Nullable 与 Android 注解帮助编译时检查 - 两家好像步调开始一致一段时间了

iOS Swift _Nullable 与 Android 注解帮助编译时检查 - 两家好像步调开始一致一段时间了 太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS.Android.HTML5.Arduino.pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作. 首先声明个人看法,在 Andr

关于在android源码编译时引用第三方jar宝--需要在android.mk中配置

今天进行android源码编译时出现一个问题,报错找不到文件,最后查看到那些找不到的问题全部都是第三方jar包里的引用文件,于是百度找解决办法.终于找到解决办法,现在分享给大家,我已经亲测通过了. 转自:http://www.cnblogs.com/hopetribe/archive/2012/04/23/2467060.html 开始正文: LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_STATIC_JAVA_LIBRARIES

android ndk 引入已有的.so文件,编译时消失,求大牛

问题描述 android ndk 引入已有的.so文件,编译时消失,求大牛 我有个android ndk工程,引入了已有的.so文件(copy 到libsarmeabi文件下).编译的时候,就只生成了自有的.so文件,copy的已有的.so文件都消失.我怎样才能引入第三方.so文件呢,求大牛 解决方案 你先生成自有的,再拷入已有的 解决方案二: 是不你的Android.mk文件没写正确 解决方案三: 若是使用cdt builder那再正常不过了,cdt builder先将lib/armeabi清