大家还记得[JakeWharton](https://github.com/JakeWharton)
写的翻遍ui注解的库butterknife
么? 之前我们都是使用findviewById来查找view,有了butterknife
我们摆脱了这种重复代码,通过注解我们解放了生产力,我们可以把更多的经理放到我们的业务和整体的架构上来,而不是不断重复的代码。
在开始前我们需要了解一下Annotation Processor
,使用 Annotation Processor 来自动生成可以提高编写代码的效率和质量,手工编写毕竟容易出现纰漏,工具自动生成是有质量保证的。Annotation Processor 主要涉及 3 部分,注解本身(Annotation)、注解处理器(Annotation Processor)以及 如何使用注解器。
一、Hello world 万事先从一个HelloWorld开始,我们可以通过Annotation Processor
来自己生成一个类,这个类中的main方法可以打印hello world。
1. 声明注解 需要我们通过Android Studio来新建一个java lib项目 helloAnotation。 其中需要声明一个注解如下:
1 2 3 4 5 @Target (ElementType.TYPE)@Retention (RetentionPolicy.SOURCE)public @interface HelloWorld { int version () default 0 ; }
2. Annotation Processor 再来一个java lib项目我们给它起名字为helloAnotation
,这个库中的核心是一个需要继承AbstractProcessor
的类,为了快捷实现这个类我们还需要借助javapoet
这个工具包。
后面会详细介绍AbstractProcessor
和javapoet
,我们先来自己定义一个Processor类。
Processor类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class HelloWord extends AbstractProcessor { @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { MethodSpec main = MethodSpec.methodBuilder("main" ) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void .class) .addParameter(String[].class, "args" ) .addStatement("$T.out.println($S)" , System.class, "Hello, World! " ) .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorldC" ) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld" , helloWorld) .build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } return false ; } …… }
声明processor类 如今已经编写完毕了 processor,需要告知系统,我们创建/src/main/resources/META-INF/services
文件夹,同时在这个文件夹下创建javax.annotation.processing.Processor
文件,在这个文件下,写入我们的processor的完全路径。
1 me.cyning.anotationcore.HelloWord
整个helloAnotation
库的build.gradle
需要配置如下:
1 2 implementation 'com.squareup:javapoet:1.7.0' implementation 'com.google.guava:guava:23.3-android
使用 在主项目的入口类TestActivity的类使用这个注解:
1 2 @HelloWorld public class TestActivity extends AppCompatActivity
build.gradle
1 2 annotationProcessor project (':AnotationCompiler' ) implementation project (': helloAnotation' )
再次编译就可以在你的主项目的build/generated/source/apt/debug下看到我们生成的类。
理解Annotation Processor 在这个helloWorld
的 🌰 中,我们需要掌握两个知识点:AbstractProcessor
和javapoet
.
AbstractProcessor AbstractProcessor 函数实现 AbstractProcessor
是自定义Annotation Processor
核心,它是运行在我们的编译代码阶段,也就是说我们每次在编译前,我们的编译器会根据我们实现的AbstractProcessor
类来生成相关类。
AbstractProcessor
是个抽象类,有几个函数使我们必须要实现的。
init(ProcessingEnvironment processingEnvironment) 这个可以认为是初始化工作,通过processingEnvironment
我们可以得到很多有用的工具类,如Elements,Types和Filer,这个后面我们是会接触到的。
process(Set<? extends TypeElement> annotations, RoundEnvironment env)
: 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment
,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
getSupportedAnnotationTypes
是给那些注解使用的,因为可能一个项目中有多个注解器,所有这个是检查步骤,只有在getSupportedAnnotationTypes
中就可以继续执行下面的操作。如刚才的helloworld项目中的HelloWorld
注解,需要我们声明下:
1 2 3 4 @Override public Set<String> getSupportedAnnotationTypes () { return Collections.singleton("me.cyning.anotationan.HelloWorld" ); }
getSupportedSourceVersion
指定java版本。通常我们设置为:SourceVersion.latestSupported(),当然你可以自己指定,如指定java 7 SourceVersion.RELEASE_7.
在java 7 之后呢,我们可以通过注解来实现对getSupportedAnnotationTypes
和getSupportedSourceVersion
配置。
@SupportedSourceVersion(SourceVersion.RELEASE_7) @SupportedAnnotationTypes({“me.cyning.anotationan.HelloWorld”}) public class HelloWord extends AbstractProcessor{ …… }
变量 由于实现自定义的AbstractProcessor
需要借助javax.annotation.processing
等jdk的工具包的类,有几个变量和工具类可能是需要了解的。
Element
.在获取我们的注解变量时,我们需要根据我们的实际情况来判断,如这个变量是方法,类或者包时,需要借助如下几个类:
Element代表java源文件中的程序构建元素,例如包、类、方法等。
TypeMirror
: TypeMirror
用来描述类信息,如判断类继承关系等。 通过Element.asType()接口可以获取Element的TypeMirror。
当TypeMirror是DeclaredType或者TypeVariable时,TypeMirror可以转化成Element:Element element = processingEviroment.getTypeUtils().asElement(typeMirror);
工具类:有几个工具类对我们的开发很有用。
Log
通过 init方法中获取Messager()对象,messager = processingEnv.getMessager()
,在需要打日志的地方打印只需要 messager.printMessage(Diagnostic.Kind.NOTE, logStr)
通过string获取TypeMirror
如需要获得android.view.View
对应的TypeMirror
,只需要在init方法中加入如下代码:
1 2 elementUtils = processingEnv.getElementUtils(); mIViewBindType = elementUtils.getTypeElement(``android.view.View``).asType();
javapoet
javapoet
是square公司开源的一个可以生产java文件的库,在Github上的地址:square/javapoet .他是一个很好的工具,有个它,会让你事半功倍。 如在刚才例子中生产的一个类的写法:
1 2 3 4 5 6 7 8 9 10 MethodSpec main = MethodSpec.methodBuilder("main" ) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void .class) .addParameter(String[].class, "args" ) .addStatement("$T.out.println($S)" , System.class, "Hello, World! " ) .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorldC" ) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build();
只需要添加修饰符(addModifiers
)、传参(addParameter
)、添加代码块(addStatement
)这样一个方法搞定,至于类的生成似乎更简单。 这样优雅的写法,有没有电到你。
仿ButterKnife 在仿butterknife之前我们可以先预习下我们自定义Annotation Processor
的三部曲:
自定义注解
实现Processor类,将这个Processor
类放到到resources/META-INF
下指定的文件(一个Processor
类一行,多个需要写多行)
写我们的入口类,如butterKnife的ButterKnife.bind(Activity activity)
目的 : 仿ButterKnife
学习写自己的注解库, 期望结果,我们写如下类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class MainActivity extends AppCompatActivity implements View .OnClickListener { @Inject (R.id.btnTest) public Button btnTest; @Inject (R.id.btnTest2) public Button btnTes2; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); DroidFinder.bind(this ); btnTes2.setOnClickListener(this ); btnTest.setOnClickListener(this ); } @Override public void onClick (View v) { Toast.makeText(MainActivity.this , v.getId() +" " , Toast.LENGTH_LONG).show(); } }
能通过DroidFinder.bind(this)
这个入口和 Inject
生成的类,自己去实现哪些findViewById这个重复代码。
自定义注解 由于我们是类中的变量来注解,所以可以直接使用FIELD
,而注解 Inject
后的value是view的id为整形,所以可以自定义注解如下:
1 2 3 4 5 @Target (ElementType.FIELD)@Retention (RetentionPolicy.SOURCE)public @interface Inject { int value () default 0 ; }
这个注解需要放到一个单独的java lib下,我们命名为inject-anotation
.
实现Processor类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 mport com.google.auto.service.AutoService; import com.squareup.javapoet.JavaFile;import com.squareup.javapoet.MethodSpec;import com.squareup.javapoet.TypeName;import com.squareup.javapoet.TypeSpec;import java.io.IOException;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;import javax.annotation.processing.AbstractProcessor;import javax.annotation.processing.Messager;import javax.annotation.processing.ProcessingEnvironment;import javax.annotation.processing.Processor;import javax.annotation.processing.RoundEnvironment;import javax.annotation.processing.SupportedAnnotationTypes;import javax.annotation.processing.SupportedSourceVersion;import javax.lang.model.SourceVersion;import javax.lang.model.element.Element;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.type.TypeMirror;import javax.lang.model.util.Elements;import javax.tools.Diagnostic;import me.cyning.anotationan.Inject;import me.cyning.anotationcore.inter.ClassTarget;import me.cyning.anotationcore.inter.FieldTarget;@SupportedSourceVersion (SourceVersion.RELEASE_8)@AutoService (Processor.class)@SupportedAnnotationTypes ("me.cyning.anotationan.Inject" )public class ViewProcessor extends AbstractProcessor { private ProcessingEnvironment mProEnv; Messager messager = null ; public static final String SUFFIX = "_Binder" ; public static final String BINDER_INTERFACE = "me.cyning.core.bind.IViewBind" ; private TypeMirror mIViewBindType; private Elements elementUtils; @Override public synchronized void init (ProcessingEnvironment processingEnv) { super .init(processingEnv); mProEnv = processingEnv; messager = processingEnv.getMessager(); elementUtils = processingEnv.getElementUtils(); mIViewBindType = elementUtils.getTypeElement(BINDER_INTERFACE).asType(); } @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Map<TypeElement, ClassTarget> mTargetMaps = new HashMap<>(); String packageStr = null ; String classNameStr = null ; TypeElement classElem = null ; Set<Element> mElements = (Set<Element>) roundEnv.getElementsAnnotatedWith(Inject.class); for (Element mElement : mElements) { VariableElement variableElement = (VariableElement) mElement; classElem = (TypeElement) variableElement.getEnclosingElement(); String name = variableElement.getSimpleName().toString(); int id = variableElement.getAnnotation(Inject.class).value(); TypeMirror type = variableElement.asType(); PackageElement pkgElem = mProEnv.getElementUtils().getPackageOf(classElem); packageStr = pkgElem.getQualifiedName().toString(); classNameStr = classElem.getSimpleName().toString() + SUFFIX; ClassTarget mClassTarget = mTargetMaps.get(classElem); if (mClassTarget == null ) { mClassTarget = new ClassTarget(); mClassTarget.clazzName = classNameStr; mClassTarget.packageName = packageStr; mClassTarget.classType = classElem.asType(); mTargetMaps.put(classElem, mClassTarget); } FieldTarget fieldTarget = new FieldTarget(); fieldTarget.id = id; fieldTarget.name = name; fieldTarget.type = type; mClassTarget.add(fieldTarget); log("getEnclosingElement" , classElem.getEnclosingElement().toString()); } log("getEnclosingElement" , '1' ); log("classNameStr " , classNameStr); try { if (classNameStr != null ) { Iterator entries = mTargetMaps.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<TypeElement, ClassTarget> entry = (Map.Entry<TypeElement, ClassTarget>) entries.next(); TypeElement key = (TypeElement) entry.getKey(); ClassTarget value = (ClassTarget) entry.getValue(); MethodSpec.Builder builder = MethodSpec.methodBuilder("findViews" ) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(Object.class, "obj" , Modifier.FINAL) .returns(void .class) .addStatement("$T target = (($T) obj)" , value.classType, value.classType); for (FieldTarget mFieldTarget : value.mFieldTargets) { builder.addStatement("target." + mFieldTarget.name + " = " + "($T)target.findViewById(" + mFieldTarget.id + ")" , mFieldTarget.type); } MethodSpec flux = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .build(); MethodSpec findViews = builder .build(); TypeSpec bindClazz = TypeSpec.classBuilder(value.clazzName) .addModifiers(Modifier.PUBLIC) .addMethod(findViews) .addMethod(flux) .addSuperinterface(TypeName.get(mIViewBindType)) .build(); JavaFile javaFile = JavaFile.builder(value.packageName, bindClazz) .build(); javaFile.writeTo(processingEnv.getFiler()); } } } catch (IOException e) { e.printStackTrace(); } return false ; } private void log (String tag, Object object) { if (object != null ) { messager.printMessage(Diagnostic.Kind.NOTE, tag + " --- " + object.toString()); } } }