在你的工程中,是否有一些文件代码具有配置化,模板化的特点,这些代码不再有逻辑上的变动,只是随着业务的发展,重复的堆叠。当你在这个文件中新增一行配置时,内心是否心生抗拒,思考过这行配置是否可以不用人工来添加,让你从机械重复的劳动中解放出来呢?
本文通过介绍腾讯视频项目中,adapter创建View的例子,向大家介绍,如何通过自定义注解处理器自动生成代码,以及如何调试自定义注解处理器。首先,介绍一下我们工程中,Adapter是如何创建View的。
onCreateViewHolder方法中,将viewType传递给ONAViewTools.getONAView的静态方法,返回指定的View。
@Override
public RecyclerView.ViewHolder onCreateInnerViewHolder(ViewGroup viewGroup, int viewType) {
View convertView;
convertView = (View) ONAViewTools.getONAView(viewType, mContext);
return new RecycleViewItemHolder(convertView);
}
在我们的工程中,频道页上所有的view,都是通过ONAViewTools这个工具类创建出来的。
ONAViewTools类:
public static IONAView getONAView(int viewType, Context context) {
.........
return createONAView(viewType, context);
}
这里省略了一些细节。
createONAView方法:
public static IONAView createONAView(int viewType, Context context) {
try {
if (context != null) {
switch (viewType) {
case EONAViewType._EnumONAMultPoster:
return new ONAMultPosterView(context);
case EONAViewType._EnumONAGalleryPoster:
return new ONAGalleryPosterView(context);
.......此处省略了很多条case:
return new ONASplitLineView(context);
case EONAViewType._EnumONAStarList:
return new ONAStarListView(context);
case EONAViewType._EnumONANewsItem:
return new ONANewsItemView(context);
}
}
}catch(Exception e){
}
}
那么,当我们新增一种View的时候,套路已经清晰了。当我们新增一种ONAXXXView时,要经历以下几个步骤:
现在,我们就开始说明,如何自动化的在ONAViewTools中新增配置。当然,你可能觉得,每次在ONAViewTools中手动新增一条配置也没花多少时间。确实,我这里只是拿来举例子,配合讲明白这篇文章的主题。并且本文将通过新工程的方式讲解,而不是基于腾讯视频的工程。
首先,介绍一下需要用到的基础知识。
android-apt是Android Studio中一款用来辅助处理编译时注解的Gradle插件。不知注解为何物的同学可以先下去补补课。Github上非常著名的EventBus、ButterKnife、Retrofit等优秀开源库都使用了这个插件,它们都是基于编译时注解实现的框架。
Annotation Processing Tool 是jdk5.0之后提供的用于编译期处理注解的api组件,简称apt。主要包括两大部分:
1、用于模型化Java 程序语言结构的模型化api,包括com.sun.mirror包下的mirror api,javax.lang.model包下的element api 及其他辅助工具类。
2、javax.annotation.processing包下用于编写注解处理器的注解处理api。
Element代表java源文件中的程序构建元素,例如包、类、方法等。Element接口有5个子类。
PackageElement | 包程序元素 |
---|---|
TypeElement | 类、接口、注解、枚举元素 |
VariableElement | 方法参数、成员变量、局部变量、枚举常量、异常参数 |
ExecutableElement | 方法、构造函数、静态代码块 |
|TypeParameterElement 类、接口、方法、或构造方法的泛型参数|
TypeMirror 用于描述Java程序中元素的信息,即Element 的元信息。通过Element.asType()接口可以获取Element的TypeMirror。TypeMirror接口的继承结构相对比较复杂:
一些已知的TypeMirror的释意:
PrimitiveType | 原始数据类型,boolean,byte,short int,long,float,char,double |
---|---|
ReferenceType | 引用类型 |
ArrayType | 数组类型 |
DeclaredType | 声明的类型,例如类、接口、枚举、注解类型 |
AnnotationType | 注解类型 |
ClassType | 类类型 |
EnumType | 枚举类型 |
InterfaceType | 接口类型 |
TypeVariable | 类型变量类型 |
VoidType | void 类型 |
WildcardType | 通配符类型 |
当TypeMirror是DeclaredType或者TypeVariable时,TypeMirror可以转化成Element:
Element element = processingEviroment.getTypeUtils().asElement(typeMirror);
JavaPoet是一组用来生成 .java文件的JAVA API。正如其名,当你创建.java文件时,你将不用再处理代码换行、缩进、引用导入等枯燥而又容易出错的工作,这一切JavaPoet都将能够很好地为你完成,你的工作将变得富有诗意。
TypeSpec、ParameterSpec、MethodSpec、CodeBlock、JavaFile都是JavaPoet提供的用于描述一个源文件元素的类。TypeSpec代表了一个接口、类、注解、枚举的定义,ParameterSpec代表一个成员变量、函数参数的定义,MethodSpec代表了方法的定义,CodelBlock用于描述一段代码块,JavaFile表示源文件本身。一个java文件正式通过以上几种类型的嵌套、组合,最终描述成一个完整的java源文件。JavaPoet为每种元素,都提供了相应的Builder类。
JavaPoet提供了一套自定义的字符串格式化规则。常用的有$L、$S、$T、$N:
格式化规则 | 表示 |
---|---|
$L | 字面量 |
$S | 字符串 |
$T | 类、接口 |
$N | 变量 |
介绍完基础知识,下面我们通过新建工程的方式,一步步讲解:
1 .新建工程AutoTypeBinding。
2 .新建viewtypebinder model,选择java library,该model中,提供注解ViewType的定义:
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ViewType {
int value() default -1;
}
3 .新建apt_compiler model,选择java library,该model 中新建ViewTypeProcessor类,该类需继承annotation.processing.AbstractProcessor类。
在apt_compiler model 的build.gradle中,添加如下配置:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':viewtypebinder')
compile 'com.squareup:javapoet:1.7.0'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
现在,你的工程结构应该如下图所示:
在ViewTypeProcessor中编写注解处理的代码:
@SupportedAnnotationTypes("com.example.ViewType")
@SupportedOptions({"fileName","failedView"})
public class ViewTypeProcessor extends AbstractProcessor {
private Messager mMessager;
private static final String GENERATED_FILE_NAME = "fileName";
private static final String FAILED_VIEW = "failedView";
private String mFileName;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("process");
Collection<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(ViewType.class);
List<TypeElement> types = ElementFilter.typesIn(annotatedElements);
if(types == null || types.isEmpty()){
return false;
}
mFileName = processingEnv.getOptions().get(GENERATED_FILE_NAME);
if(mFileName == null || mFileName.isEmpty()){
mMessager.printMessage(Diagnostic.Kind.WARNING, "No option generatedFileName passed to annotation processor");
return false;
}
//解析failedView参数 -------- ①
ClassName failedView = null;
String failedViewName = processingEnv.getOptions().get(FAILED_VIEW);
try {
if(failedViewName != null && !failedViewName.isEmpty()){
failedView = ClassName.bestGuess(failedViewName);
}
}catch (IllegalArgumentException e){
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(Locale.getDefault(), "Option is invalid: %s", failedViewName));
throw new AbortProcessViewTypeException(e);
}
TypeElement failedViewTypeElement = processingEnv.getElementUtils().getTypeElement(failedViewName);
checkTypeVaild(failedViewTypeElement);
ClassName className = null;
try {
className = ClassName.bestGuess(mFileName);
System.out.println("Generate file name: " + mFileName);
}catch (IllegalArgumentException e){
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(Locale.getDefault(), "Option is invalid: %s", mFileName));
RuntimeException processException = new AbortProcessViewTypeException();
processException.initCause(e);
throw processException;
}
ClassName View = ClassName.bestGuess("android.view.View");
ClassName Context = ClassName.bestGuess("android.content.Context");
//构建类
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addJavadoc("This file is generated by apt, please do not modify!");
ParameterSpec paramsContext = ParameterSpec.builder(Context, "context").build();
ParameterSpec typeParamSpec = ParameterSpec.builder(TypeName.INT, "type").build();
//构建方法 public static final createView(Context context, int type)
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("createView")
.addModifiers(Modifier.FINAL, Modifier.PUBLIC, Modifier.STATIC)
.returns(View)
.addParameter(paramsContext)
.addParameter(typeParamSpec);
//如果指定了failedView,则如果创建View发生异常时,返回failedView
if(failedView != null){
methodBuilder.beginControlFlow("try");
}
//构建switch(type){
// }
//代码块
CodeBlock.Builder caseBlock = CodeBlock.builder().beginControlFlow("switch($N)", typeParamSpec);
//循环遍历所有被注解元素,每项元素都会对应生成一条case 语句
for (TypeElement type : types){
ViewType viewType = type.getAnnotation(ViewType.class);
if(viewType == null){
continue;
}
checkTypeVaild(type);
int value = viewType.value();
ClassName viewName = ClassName.get(type);
System.out.println("viewName = " +viewName.simpleName() + ", value = " + value);
caseBlock.add("case $L:\n", value).indent().addStatement("return new $T($N)", viewName, paramsContext).unindent();
}
//没有匹配到的情况下,返回null
caseBlock.add("default:\n").indent().addStatement("return null", View, paramsContext).unindent();
caseBlock.endControlFlow();
methodBuilder.addCode(caseBlock.build());
if(failedView != null) {
methodBuilder.nextControlFlow("catch($T t)", Throwable.class);
methodBuilder.addStatement("return new $T($N)", failedView, paramsContext);
methodBuilder.endControlFlow();
}
//构建文件
JavaFile javaFile = JavaFile.builder(className.packageName(), typeBuilder.addMethod(methodBuilder.build()).build()).build();
//写文件
writeSourceFile(mFileName, javaFile.toString());
return false;
}
process方法通过遍历所有被@ViewType注解的View,生成一个由mFileName指定的java文件,该文件中包含一个静态方法public static final View createView(Context context, int type)的方法 ,该方法体由一个switch语句根具type的值创建并返回不同类型的View。
apt提供了@SupportedAnnotationTypes、@SupportedSourceVersion、@SupportOptions三个注解分别用来注明该Processor文件支持的注解类型,支持的java版本,和支持的输入参数。你也可以通过覆写AbstractProcessor的getSupportedAnnotationTypes(),getSupportedSourceVersion(),getSupportOptions()方法来指定。可以看到,我们通过@SupportedAnnotationTypes注解描述了ViewTypeProcessor需要处理的注解类是 com.example.ViewType,该注解处理器需要指定的输入参数有fileName和failedView,fileName指定了生成java文件的名称,failedView指定了当创建View发生异常时,需要返回一个默认View,在debug模式下,这很有用,比如,你在listView上看到了一个failedView,表明该位置position创建View失败了。
在void init(ProcessingEnviroment processingEnv)方法中,为了方便我们向控制台输出日志,我们将Messager保存起来。apt工具初始化processor时,会回调init方法, processingEnv是apt向processor传递的编译环境参数,processingEnv向processor提供了访问apt编译环境的工具集,比如,通过processingEvn.getMessager()可以获得向控制台报告错误、警告、提示的工具,通过processingEvn.getFiler()可以获得创建java源文件的工具。
接下来,我们来看process方法。process方法可能会被apt工具多次调用,,apt初始化的时候,会调用一次process方法。在第一次调用时,apt编译器会将整个工程作为输入,收集到所有被ViewType注解的元素,然后同过process方法的参数annotations传递给process方法处理。如果在某轮process处理中,process生成了新的java文件,则apt编译器会将新生成的java文件作为输入,然后收集到新的被注解的元素,直到不再产生新的文件后,process循环调用结束。注意,当没有新的文件生成后,process还会被再调用一次,此次输入是空的。
round | input | output |
---|---|---|
1 | 整个项目 | A.java |
2 | A.java | none |
3 | none | none |
在代码①处,我们解析输入参数FAILED_VIEW,如过FAILED_VIEW被指定,则会尝试通过ClassName这个类的bestGuess方法,这个方法接受一个字符串failedViewName,返回一个ClassName failedView,failedView完整的描述了failedViewName代表的类名称、包名称等信息。如果failedViewName格式不合法,bestGuess会抛出IllegalArgumentException。
下面看这两行代码:
TypeElement failedViewTypeElement = processingEnv.getElementUtils().getTypeElement(failedViewName);
checkTypeVaild(failedViewTypeElement);
通过processingEvn的getElementUtis()方法获取操作程序元素Element的工具类Elements,并通过getTypeElement()方法返回由failedViewName指定的TypeElement 元素。checkTypeVaild()会校验这个元素是否合法,如果不合法,chechTypeVaild会抛出异常,终止process处理。
private void checkTypeVaild(TypeElement type) {
if(type.getKind() != ElementKind.CLASS){
reportErrorWithAbort("The annotation @ViewType only applies to classes", type);
}
if(!ancestorIsView(type)){
reportErrorWithAbort("The annotation @ViewType only applies to Views", type);
}
if(!isVisible(type)){
reportErrorWithAbort("cannot resolve symbol " + type.getQualifiedName(), type);
}
}
checkTypeVaild所接受的元素必须是类,而且必须继承自android.view.View,并且必需相对生成的java文件可见,也就是生成的java文件必须对type所表示的类具有访问权限。
元素type是否继承自View:
private boolean ancestorIsView(TypeElement type) {
while (true){
TypeMirror parentMirror = type.getSuperclass();
if(parentMirror.getKind() == TypeKind.NONE){
return false;
}
TypeElement parentElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentMirror);
if(parentElement.getQualifiedName().contentEquals("android.view.View")){
return true;
}
type = parentElement;
}
}
元素是否可见:
private boolean isVisible(TypeElement type){
String myPackage = ClassName.bestGuess(mFileName).packageName();
boolean isNestClass = type.getEnclosingElement().getKind() == ElementKind.CLASS;
Set<Modifier> modifiers = type.getModifiers();
if(isNestClass && !modifiers.contains(Modifier.STATIC)){
return false;
}
if(modifiers.contains(Modifier.PUBLIC)){
return true;
}
if(ClassName.get(type).packageName().contentEquals(myPackage)){
return true;
}
return false;
}
type.getEnclosingElement()返回包裹type的最里层元素,如果该元素恰巧是一个类,那么type就是一个内部类。type.getModifiers()返回该元素的访问权限修饰符,Modifiers是java反射包中提供的类,定义了PRIVATE,PUBLIC等常量,分别对应private、public修饰符。如果type是一个内部类,则其必须是一个静态类。其次,如果type是一个public类,则可以访问,否则,看type是否和mFileName指定的java文件是否在同一个包下。
TypeSpec、ParameterSpec、MethodSpec、CodeBlock、JavaFile都是JavaPoet提供的用于描述一个源文件元素的类。TypeSpec代表了一个接口、类、注解、枚举的定义,ParameterSpec代表一个成员变量、函数参数的定义,MethodSpec代表了方法的定义,CodelBlock用于描述一段代码块,JavaFile表示源文件本身。一个java文件正式通过以上几种类型的嵌套、组合,最终描述成一个完整的java源文件。JavaPoet为每种元素,都提供了相应的Builder类。
JavaFile javaFile = JavaFile.builder(className.packageName(), typeBuilder.addMethod(methodBuilder.build()).build()).build();
writeSourceFile(mFileName, javaFile.toString());
当javaFile构建好后,则通过writeSourceFile()方法,生成源文件。
private void writeSourceFile(String fileName, String s) {
try {
JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(fileName);
Writer writer =fileObject.openWriter();
try {
writer.write(s);
}finally {
try {
writer.close();
}catch (Exception e){
e.printStackTrace();
}
}
}catch (IOException e){
mMessager.printMessage(Diagnostic.Kind.WARNING, "could not write class " + fileName);
}
}
通过processingEnv.getFiler()会返回一个Filer接口,Filer的createSrouceFile方法可以创建java文件对象fileObject,这样,我们用可以将javaFile生成的字符串写到文件中去了。为什么这里需要通过processingEnv提供的Filer接口来写文件呢,我们完全可以通过自己new File()的方式创建文件呀?答案是确实可以,但是这样,apt就无法感知有新的源文件创建了。
ViewTypeProcessor文件代码编写完后,还有一件非常重要的事情,就是注册ViewTypeProcessor,这样javac编译器才能在编译的时候找到正确的注解处理器处理注解。
3 .在文件中新增一下语句:
com.example.ViewTypeProcessor
以上配置过程也可通过引入插件自动完成。AutoService是google提供已一款可以自动生成jar包配置的插件。首先在apt-compiler build.gradle文件下,添加如下红框中依赖:
然后再ViewTypeProcesspor上新增如下注解:
现在,我们来编写我们的主工程,来测试我们的apt_compiler处理器。工程结构如下所示:
MyNameView.java定义如下:
@ViewType(MyViewTypes.MY_NAME_VIEW)
public class MyNameView extends TextView {
public MyNameView(Context context) {
this(context, null, 0);
}
public MyNameView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyNameView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
setText("吴涛");
setTextColor(Color.BLACK);
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
setGravity(Gravity.CENTER);
}
}
FailedView.java
public class FailedView extends View {
public FailedView(Context context) {
super(context);
}
public FailedView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FailedView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
app builg.gradle:
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' //引入android-apt插件
android {
.................
}
apt {
arguments {
fileName "com.example.wutao.autotypebinding.ViewTypeTools" //注解处理器生成的java文件名
failedView "com.example.wutao.autotypebinding.view.FailedView" //指定异常View
}
}
dependencies {
........
........
provided project(":viewtypebinder")
apt project(':apt_compiler')
}
编译后,成功在app/build/generated/source/apt/debug/com/example/wutao/autotypebinding/目录下生成ViewTypeTools.java文件:
package com.example.wutao.autotypebinding;
import android.content.Context;
import android.view.View;
import com.example.wutao.autotypebinding.view.FailedView;
import com.example.wutao.autotypebinding.view.MyNameView;
import java.lang.Throwable;
/**
* This file is generated by apt, please do not modify! */
public final class ViewTypeTools {
public static final View createView(Context context, int type) {
try {
switch(type) {
case 11:
return new MyNameView(context);
default:
return null;
}
} catch(Throwable t) {
return new FailedView(context);
}
}
}
现在,我们可以在MainActivity.java中引用该类了:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View view = ViewTypeTools.createView(this, MyViewTypes.MY_NAME_VIEW);
ViewGroup root = findViewById(R.id.root);
root.addView(view);
}
}
也许在我们开发注解处理器的时候,还需要单步调试,以便我们寻找注解处理器的漏洞。下面就向大家介绍,如何调试我们刚才开发的ViewTypeProcessor注解处理器。
1、在process方法中的合适位置下断点:
下断点的方法与平常调试android代码并无区别。
2、在项目的根目录下的gradle.properties文件中,新增如下配置:
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
org.gradle.daemon=true
3、新建remote debugger:
注意新建remoteDebuger的名称一定要是AnnotationProcessor 。
4:、Debug AnnotationProcessor:
现在,我们看到断点已经生效:
5、执行CompilerDebugWithJavac任务,命中断点:
本文通过Adapter中使用工具类创建View的例子,一步一步讲解了如何通过自定义注解处理器,如何使用javaPoet提供的api,以及如何使用android-apt插件,以自动化的方式来生成工具类文件代码,从而提高编码效率。另外,本文还讲解了如何配置虚拟机参数,来调试逻辑稍复杂的自定义注解处理器。
现在有越来越多的开源项目在使用apt,apt的强大之处可见一斑,因此作为一个android开发者,我们有必要去了解这门技术。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。