apt,annotation processing tool,即注解处理器,是一种用来处理注解的工具,常用在编译时扫描和处理注解,最终生成处理注解逻辑的java文件。apt技术在目前的很多框架上都有着使用,如butterknife,arouter,greendao等框架上都有着apt技术的影子。
apt作用
使用apt可以在编译时来处理编译时注解,生成额外的java文件,有如下效果:
可以达到减少重复代码手工编写的效果。
如butterknife,我们可以直接使用注解来减少findviewbyid这些代码,只需要通过注解表示是哪个id就够了。
功能封装。将主要的功能逻辑封装起来,只保留注解调用。
相对于使用java反射来处理运行时注解,使用apt有着更加良好的性能。
android基本编译流程
android中的代码编译时需要经过:java——>class ——> dex 流程,代码最终生成dex文件打入到apk包里面。
apt是在编译开始时就介入的,用来处理编译时注解。
aop(aspect oridnted programming)是在编译完成后生成dex文件之前,通过直接修改.class文件的方式,来对代码进行修改或添加逻辑。常用在在代码监控,代码修改,代码分析这些场景。
apt基本使用
基本使用流程主要包括如下几个步骤:
- 创建自定义注解
- 创建注解处理器,处理java文件生成逻辑
- 封装一个供外部调用的api
- 项目中调用
本次以仿写butterknife绑定view为例,省略findviewbyid的代码,调用代码如下:
public class annotationtestactivity extends appcompatactivity { @bindview(r.id.tv_annotation_test1) textview tvannotationtest1; @bindview(r.id.tv_annotation_test2) textview tvannotationtest2; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_annotation_test); bindviewtools.bind(this); tvannotationtest1.settext("测试文本"); tvannotationtest1.setonclicklistener(new view.onclicklistener() { @override public void onclick(view v) { logger.toast(workdemoapplication.context,"控件1:" r.id.tv_annotation_test1); } }); tvannotationtest2.settext("另一个文本"); tvannotationtest2.setonclicklistener(new view.onclicklistener() { @override public void onclick(view v) { logger.toast(workdemoapplication.context,"控件2:" r.id.tv_annotation_test2); } }); } }
1、 自定义注解
我们新建立一个java lib,命名为annotationtest
,用来承载自定义注解,代码如下所示:
@retention(retentionpolicy.class) @target(elementtype.field) public @interface bindview { int value(); }
2、注解处理器
额外新建立一个java lib,命名为processortest
,用来承载注解处理及java文件生成逻辑。
主要包括如下几个步骤:
- 添加注解处理器
- 注解处理器注册
- 添加java代码生成逻辑
需要注意的是,当前的注解处理器lib需要引入注解lib——annotationtest
,在当前module的build.gradle
文件中配置:
implementation project(':annotationtest')//依赖刚刚创建的annotation模块
注解处理器
注解处理器代码如下:
public class bindviewprocessor extends abstractprocessor { private messager mmessager; private elements melementutils; private mapmproxymap = new hashmap<>(); @override public synchronized void init(processingenvironment processingenv) { super.init(processingenv); mmessager = processingenv.getmessager(); melementutils = processingenv.getelementutils(); } @override public set getsupportedannotationtypes() { hashset supporttypes = new linkedhashset<>(); supporttypes.add(bindview.class.getcanonicalname()); return supporttypes; } @override public sourceversion getsupportedsourceversion() { return sourceversion.latestsupported(); } @override public boolean process(set set, roundenvironment roundenvironment) { mmessager.printmessage(diagnostic.kind.note, "processing..."); mproxymap.clear(); //得到所有的注解 set 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); } //通过遍历mproxymap,创建java文件 for (string key : mproxymap.keyset()) { classcreatorproxy proxyinfo = mproxymap.get(key); javafile javafile = javafile.builder(proxyinfo.getpackagename(),proxyinfo.generatejavacode2()).build(); try { //生成java文件 javafile.writeto(processingenv.getfiler()); } catch (ioexception e) { mmessager.printmessage(diagnostic.kind.note, " --> create " proxyinfo.getproxyclassfullname() "error"); } } mmessager.printmessage(diagnostic.kind.note, "process finish ..."); return true; } }
此处为了代码演示简单起见,并没有加入格式校验(如对注解修饰的类型等信息进行校验),如果你实际运用apt技术,请务必要对注解的使用规则进行详细的校验。
注解处理器注册
自定义的注解处理器必须经过注册才能够使用,即需要对注解处理器添加自动主动的注解。
我们可以使用google autoservice来进行注解处理器的自动注册,首先需要在注解处理器所在的module的build.gradle
文件添加autoservice的包引入:
//google autoservice implementation "com.google.auto.service:auto-service:1.0-rc4" annotationprocessor "com.google.auto.service:auto-service:1.0-rc4"
然后将自动注册的注解添加到注解处理器上以实现自动注册效果:
@autoservice(processor.class) public class bindviewprocessor extends abstractprocessor { ... }
java代码生成
对于java代码的生成存在有多种方式,如字符串拼接,javapoet等。
如果要使用javapoet,则需要在当前的module的build.gradle
文件中引入:
//javapoet implementation "com.squareup:javapoet:1.13.0"
详细代码如下:
package com.example.shapetest.bindview; import com.squareup.javapoet.classname; import com.squareup.javapoet.methodspec; import com.squareup.javapoet.typespec; import java.util.hashmap; import java.util.map; import javax.lang.model.element.modifier; import javax.lang.model.element.packageelement; import javax.lang.model.element.typeelement; import javax.lang.model.element.variableelement; import javax.lang.model.util.elements; public class classcreatorproxy { private string mbindingclassname; private string mpackagename; private typeelement mtypeelement; private mapmvariableelementmap = 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代码 字符串拼接方式 * @return */ public string generatejavacode() { stringbuilder builder = new stringbuilder(); builder.append("package ").append(mpackagename).append(";\n\n"); builder.append("import com.example..*;\n"); builder.append('\n'); builder.append("public class ").append(mbindingclassname); builder.append(" {\n"); generatemethods(builder); builder.append('\n'); builder.append("}\n"); return builder.tostring(); } /** * 加入method 字符串拼接方式 * @param builder */ private void generatemethods(stringbuilder builder) { builder.append("public void bind(" mtypeelement.getqualifiedname() " host ) {\n"); for (int id : mvariableelementmap.keyset()) { variableelement element = mvariableelementmap.get(id); string name = element.getsimplename().tostring(); string type = element.astype().tostring(); builder.append("host." name).append(" = "); builder.append("(" type ")(((android.app.activity)host).findviewbyid( " id "));\n"); } builder.append(" }\n"); } public string getproxyclassfullname() { return mpackagename "." mbindingclassname; } public typeelement gettypeelement() { return mtypeelement; } /** * 创建java代码 javapoet * @return */ public typespec generatejavacode2() { typespec bindingclass = typespec //class名称设置 .classbuilder(mbindingclassname) //类为public .addmodifiers(modifier.public) .addmethod(generatemethods2()) .build(); return bindingclass; } /** * 加入method javapoet */ private methodspec generatemethods2() { classname host = classname.bestguess(mtypeelement.getqualifiedname().tostring()); methodspec.builder methodbuilder = methodspec //方法名称 .methodbuilder("bind") //方法为public .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 "));\n"); } return methodbuilder.build(); } public string getpackagename() { return mpackagename; } }
3. 对外调用
在完成了上述两步之后,apt的实际工作已经能够正常运行了,下面我们实现以下调用方法,以达到模仿butterknife的调用效果。
首先我们需要新建立一个android lib的module,命名为apt_lib。
当前的对外lib需要引用注解处理器lib——processortest
,在当前module的build.gradle
文件中进行如下配置:
implementation project(path: ':processortest')
新建对外的调用方法,代码如下:、
public class bindviewtools { 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 e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } catch (instantiationexception e) { e.printstacktrace(); } catch (nosuchmethodexception e) { e.printstacktrace(); } catch (invocationtargetexception e) { e.printstacktrace(); } } }
其主要功能是为被注解绑定的activity生成对应的辅助类以实现apt被调用的效果。
4. 调用
在进行了如下几步之后,我们实际上已经能够正常的调用@bindview
这个注解了.
我们在我们的主module——app
中调用apt,当然此时的apt需要引入之前的lib工程,可以在app的build.gradle
文件中进行如下配置:
implementation project(':annotationtest') // 添加依赖模块 implementation project(':apt_lib') // 添加依赖模块 implementation project(':processortest') // 添加依赖模块 annotationprocessor project(':processortest')
需要注意的是annotationprocessor这一注解处理工具是java语言使用的,如果你使用的是kotlin语言,则需要使用kapt.
此时直接调用我们在开头的调用代码,在编译执行后能够正常运行。
当我们查看工程内的build
文件夹时能够在如下路径发现我们之前生成的文件:
看到里面的代码如下:
package com.example.workdemo.activity; public class annotationtestactivity_viewbinding { public void bind(annotationtestactivity host) { host.tvannotationtest1 = (android.widget.textview)(((android.app.activity)host).findviewbyid( 2131231102)); host.tvannotationtest2 = (android.widget.textview)(((android.app.activity)host).findviewbyid( 2131231103)); } }
这样的话我们就完成了apt的一个简单示例。
总结
本文主要对android中的apt技术进行了简单说明。apt技术能够利用编译的技术直接生成java逻辑代码,在代码重复度较高的场景中比较适用。