天天看点

利用APT实现Android编译时注解 一、APT概述 二、实现目标 三、 项目框架 四、自定义注解模块 五、注解处理器模块 六、API模块 七、项目中的使用 八、总结

processing tool )。

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

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

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

然后我们利用apt生成<code>mainactivity</code>的代理类 <code>mainactivity$$proxy</code>,让该类帮我们实现冗余的功能:

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模块。

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

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

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

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

auto-service是google公司出的第三方库,主要用于注解处理器,可以自动帮我们生成meta-inf 配置信息。

注解处理器要根据自定义注解进行解析,所以也需要依赖该模块。

让我们看一下注解处理器的api。所有的注解处理器都必须继承<code>abstractprocessor</code>,如下所示:

<code>@autoservice(processor.class)</code> 属于auto-service库,可以自动生成meta-inf/services/javax.annotation.processing.processor文件(该文件是所有注解处理器都必须定义的),免去了我们手动配置的麻烦。

<code>init(processingenvironment processingenvironment)</code> 在处理器初始化的调用,通过<code>processingenv</code>参数我们可以拿到一些实用的工具类<code>elements</code>, <code>messager</code>和<code>filer</code>。我们在后面将会使用到它们。<code>elements</code>,一个用来处理<code>element</code>的工具类。<code>messager</code>,一个用来输出日志信息的工具类。<code>filer</code>、如这个类的名字所示,你可以使用这个类来创建文件。

<code>process(set&lt;? extends typeelement&gt; set, roundenvironment roundenvironment)</code> 这是注解处理器的主方法,你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。

<code>getsupportedannotationtypes()</code> 在这个方法里面你必须指定哪些注解应该被注解处理器注册。它的返回值是一个string集合,包含了你的注解处理器想要处理的注解类型的全限定名。

<code>getsupportedsourceversion()</code> 用来指定你使用的 java 版本,通常我们返回<code>sourceversion.latestsupported()</code>即可。

但是考虑到兼容性问题,建议还是重写<code>getsupportedsourceversion</code>方法和<code>getsupportedannotationtypes</code>方法

在继续讲解处理器之前,我们必须先明白<code>elment</code>元素这个概念。在注解处理器中,我们扫描 java 源文件,源代码中的每一部分都是<code>element</code>的一个特定类型。换句话说:<code>element</code>代表程序中的元素,比如说

包,类,方法。在下面的例子中,我将添加注释来说明这个问题:

<code>executeableelement</code>:可以表示一个普通方法、构造方法、初始化方法(静态和实例)。

<code>packageelement</code>:代表一个包名。

<code>typeelement</code>:代表一个类、接口。

<code>variableelement</code>:代表一个字段、枚举常量、方法或构造方法的参数、本地变量、或异常参数等。

<code>element</code>:上述所有元素的父接口,代表源码中的每一个元素。

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

比如你有如下的一个类

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

通<code>roundenvironment.getelementsannotatedwith(viewbyid.class)</code>方法返回一个被<code>@viewbyid</code>注解的<code>element</code>类型的元素列表。注意这里是<code>element</code>列表,而不是类列表,<code>element</code>可以包括类、方法、变量等。

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

在<code>isvalid</code>方法中,它检查被注解的元素是否符合规则:

被注解元素的父元素必须是个类,而不能是接口、枚举。

被注解元素的父元素必须是非private 和 非static修饰。

被注解的元素只能注解非框架层元素。

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

不知道大家有没有发现,在<code>error</code>方法中利用了<code>messager</code>(<code>init</code>方法中获取到的工具类)来处理错误信息。<code>messager</code>为注解处理器提供了一种报告错误消息,警告信息和其他消息的方式。它不是注解处理器开发者的日志工具。<code>messager</code>是用来给那些使用了你的注解处理器的第三方开发者显示信息的。 

其中非常重要的是<code>kind.error</code>级别信息,因为这种消息类型是用来表明我们的注解处理器在处理过程中出错了。有可能是第三方开发者误使用了我们的<code>@viewbyid</code>注解(比如,使用<code>@viewbyid</code>注解了一个接口中的变量)。这个概念与传统的

java 应用程序有一点区别。传统的 java 应用程序出现了错误,你可以抛出一个异常。如果你在<code>process()</code>中抛出了一个异常,那 jvm 就会崩溃。注解处理器的使用者将会得到一个从 javac 给出的非常难懂的异常错误信息。因为它包含了注解处理器的堆栈信息。因此注解处理器提供了<code>messager</code>类。它能打印漂亮的错误信息,而且你可以链接到引起这个错误的元素上。回到process中:

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

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

<code>parseviewbyid(element element)</code>:在该方法中,首先需要通过<code>getproxyclass</code>方法获取一个<code>proxyclass</code>类型的对象。<code>proxyclass</code>代表了该注解元素所对应的类元素,这里我们利用面向对象的思想进行了封装。然后我们把被注解的元素也封装成一个fieldviewbinding类型的model,并放入到proxyclass中。

<code>getproxyclass(element element)</code>:该方法主要是生成或获取注解元素所对应的类,。你可以在<code>getproxyclass</code>方法中看到,利用<code>getenclosingelement</code>方法获取了该注解元素的父元素,也就是该注解元素的所在的类。我们把每一个类元素<code>typeelement</code>都封装成了proxyclass,并保存在hashmap中。

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

<code>fieldviewbinding</code>类:

<code>proxyclass</code>类中的generateproxy()方法是用于生成每个类所对应的代理类,比如类<code>mainactivity</code>就会生成类<code>mainactivity$$proxy</code>,该方法生成的详细过程会后面再讲。

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

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

proxyclass##generateproxy()

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

既然已经生成了代理类,那么我还需要提供api供使用者访问该代理类,供在activity、fragment、view中如下使用:

在<code>proxytool</code>的<code>bind</code>方法中我们需要为需要为不同的目标(比如

activity、fragment 和 view 等)提供重载的注入方法,这些方法最终都调用<code>createbinding</code>方法:

我们重载了三个<code>bind</code>方法用于接收的不同目标(activity、fragment 和 view 等),<code>target</code>参数表示注解元素所在的类,<code>root</code>参数表示要查找的view的,因为activity和view本身即是target也是root,所以只要一个参数即可,而fragment中类和view是分离的,所以需要两个参数。

所有bind方法最终调用了<code>createbinding</code>方法,在该方法中我们通过<code>targetclass.getname() + suffix</code>,拼接成代理类的全限定名,然后生成代理类的实例,并执行代理类的<code>inject</code>方法,该方法中执行的就是findviewbyid的操作。

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

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

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

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

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

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

执行make project操作后,在build/generated/source/apt/debug/下就会生成所对应的代理类<code>mainactivity$$proxy</code>:

参考: 

<a target="_blank" href="http://zjutkz.net/2016/04/07/%e4%b8%87%e8%83%bd%e7%9a%84apt%ef%bc%81%e7%bc%96%e8%af%91%e6%97%b6%e6%b3%a8%e8%a7%a3%e7%9a%84%e5%a6%99%e7%94%a8/?utm_source=tuicool&amp;utm_medium=referral">万能的apt!编译时注解的妙用</a>