上一期咱们已经把butterknife-annotations中的注解变量都已经定义好了,分别为BindView、OnClick与Keep。java
若是你是第一次进入本系列文章,强烈推荐跳到文章末尾查看上篇文章,要否则你可能会有点云里雾里。android
若是在代码中引用的话,它将与开源库ButterKnife的操做相似。git
class MainActivity : AppCompatActivity() { @BindView(R.id.public_service, R.string.public_service) lateinit var sName: TextView @BindView(R.id.personal_wx, R.string.personal_wx) lateinit var sPhone: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Butterknife.bind(this) } @OnClick(R.id.public_service) fun nameClick(view: View) { Toast.makeText(this, getString(R.string.public_service_click_toast), Toast.LENGTH_LONG).show() } @OnClick(R.id.personal_wx) fun phoneClick(view: View) { Toast.makeText(this, getString(R.string.personal_wx_click_toast), Toast.LENGTH_LONG).show() } }
使用@BindView来绑定个人View,使用@OnClick来绑定View的点击事件。使用Butterknife.bind来绑定该Class,主要是用来实例化自动生成的类。(该部分下篇文章将说起)github
咱们本身定义的绑定注解库已经完成了1/3,接下来咱们将实现它的代码自动生成部分。这时就到了上期提到的第二个Module:butterknife-compiler。segmentfault
NameUtils是一些常量的管理工具类。api
final class NameUtils { static String getAutoGeneratorTypeName(String typeName) { return typeName + ConstantUtils.BINDING_BUTTERKNIFE_SUFFIX; } static class Package{ static final String ANDROID_VIEW = "android.view"; } static class Class { static final String CLASS_VIEW = "View"; static final String CLASS_ON_CLICK_LISTENER = "OnClickListener"; } static class Method{ static final String BIND_VIEW = "bindView"; static final String SET_ON_CLICK_LISTENER = "setOnClickListener"; static final String ON_CLICK = "onClick"; } static class Variable{ static final String ANDROID_ACTIVITY = "activity"; } }
NameUitls包含了自动生成的类名称,包名,方法名,变量名。总之就是为了代码更健全,方便管理。闭包
第二个类Processor是今天的重中之重。也是注解库代码自动生成的核心部分。因为注解的自动生成代码都是在注解进程中进行,因此这里它继承于AbstractProcessor,其中主要有三个方法须要实现。app
从简单到容易,先是init方法,咱们直接看代码ide
@Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); mMessager = processingEnv.getMessager(); mElementUtils = processingEnv.getElementUtils(); }
方法参数processingEnv为咱们提供注解处理所需的环境状态。咱们经过getFiler()、getMessager()与getElementUthis()方法,分别获取建立源代码的Filer、消息发送器Messager(主要用于向外界发送错误信息)与解析注解元素所需的通用方法。工具
例如:当咱们已经构建好了须要自动生成的类,这时咱们就可使用Filter来将代码写入到java文件中,如遇错误使用Messager将错误信息发送出去。
//写入java文 try { JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler) } catch (IOException e) { mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement); }
代码中的JavaFile与typeBuilder都是JavaPoet中的类。JavaPote主要提供Java API来帮助生成.java
资源文件。
@Override public Set<String> getSupportedAnnotationTypes() { return new TreeSet<>(Arrays.asList( BindView.class.getCanonicalName(), OnClick.class.getCanonicalName(), Keep.class.getCanonicalName()) ); }
看方法名就知道了,包含所支持的注解,将其经过set集合来返回。这里将咱们上一期自定义的注解添加到set集合中便可。
到了本篇文章的核心,process用来生成与注解相匹配的方法代码。经过解析Class中定义的注解,生成与注解相关联的类。
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { .... .... return true; }
提供了两个参数:annotations与roundEnv,分别表明须要处理的注解,这里就表明咱们自定义的注解;注解处理器所需的环境,帮助进行解析注解。
在开始解析注解以前,咱们应该先过滤咱们所不须要的注解。回头看getSupportedAnnotationTypes方法,咱们只支持BindView、OnClick与Keep这三个注解。为了解析出相匹配的注解,咱们将这个逻辑单独抽离出来,交由getTypeElementsByAnnotationType来管理。
private Set<TypeElement> getTypeElementsByAnnotationType(Set<? extends TypeElement> annotations, Set<? extends Element> elements) { Set<TypeElement> result = new HashSet<>(); //遍历包含的 package class method for (Element element : elements) { //匹配 class or interface if (element instanceof TypeElement) { boolean found = false; //遍历class中包含的 filed method constructors for (Element subElement : element.getEnclosedElements()) { //遍历element中包含的注释 for (AnnotationMirror annotationMirror : subElement.getAnnotationMirrors()) { for (TypeElement annotation : annotations) { //匹配注释 if (annotationMirror.getAnnotationType().asElement().equals(annotation)) { result.add((TypeElement) element); found = true; break; } } if (found) break; } if (found) break; } } } return result; }
首先理解Element是什么?Element表明程序中的包名、类、方法,这也是注解所支持的做用类型。而后再回到代码部分,已经给出详细代码注释。
该方法的做用就是获取到有咱们自定义注解的class。这里介绍两个主要的方法
因此经过该方法最终返回的就是MainActivity,它将被转化为TypeElement类型返回,而后将由processing来处理。
咱们再回到process方法中。经过getTypeElementsByAnnotationType()方法咱们已经获取到了咱们使用了自定义注解的TypeElement(MainActivity)。
//获取与annotation相匹配的TypeElement,即有注释声明的class Set<TypeElement> elements = getTypeElementsByAnnotationType(annotations, roundEnv.getRootElements());
下面咱们再获取构建类所需的相关信息。
//包名 String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString(); //类名 String typeName = typeElement.getSimpleName().toString(); //全称类名 ClassName className = ClassName.get(packageName, typeName); //自动生成类全称名 ClassName autoGenerationClassName = ClassName.get(packageName, NameUtils.getAutoGeneratorTypeName(typeName)); //构建自动生成的类 TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName) .addModifiers(Modifier.PUBLIC) .addAnnotation(Keep.class);
注释已经划分清楚了,能够分为四个步骤
全部信息准备完毕后,而后开始定义自动生成的类。这里经过使用TypeSpec.Builder来构建。它是JavaPoet中的类。
因为直接使用JavaFileObject生成.java资源文件是很是麻烦的,因此推荐使用JavaPoet。JavaPoet是一个开源库,主要用来帮助方便快捷的生成.java的资源文件。想要全面了解的能够查看Github连接。为了帮助快速读懂该文章,这里对其中几个主要方法进行介绍。固然在使用前还需在butterknife-compiler中的builder.gradle添加依赖:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':butterknife-annotations') implementation 'com.squareup:javapoet:1.11.1' }
同时也将上一期咱们自定义的注解Module引入。
有了上面的理解咱们再来看下面的生成代码:
//构建自动生成的类 TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName) .addModifiers(Modifier.PUBLIC) .addAnnotation(Keep.class); //添加构造方法 typeBuilder.addMethod(MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY) .addStatement("$N($N)", NameUtils.Method.BIND_VIEW, NameUtils.Variable.ANDROID_ACTIVITY) .addStatement("$N($N)", NameUtils.Method.SET_ON_CLICK_LISTENER, NameUtils.Variable.ANDROID_ACTIVITY) .build());
首先经过TypeSpec.Builder构建一个类,类名为autoGenerationClassName(MainActivity$Binding),类的访问级别为public,因为为了防止混淆使用了咱们自定义的@Keep注解。
而后再来添加类的构造方法,使用addMethod、addModifiers、addParameter与addStatement分别构建构造方法名、方法访问级别、方法参数与方法中执行的代码块。因此上面的代码最终将会自动生成以下代码:
@Keep public class MainActivity$Binding { public MainActivity$Binding(MainActivity activity) { bindView(activity); setOnClickListener(activity); } }
在自动生成类的构造方法中调用了咱们想要的bindView与setOnClickListener方法。因此接下来咱们要实现的就是这两个方法的构建。
//添加bindView成员方法 MethodSpec.Builder bindViewBuilder = MethodSpec.methodBuilder(NameUtils.Method.BIND_VIEW) .addModifiers(Modifier.PRIVATE) .returns(TypeName.VOID) .addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY); //添加方法内容 for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) { BindView bindView = variableElement.getAnnotation(BindView.class); if (bindView != null) { bindViewBuilder.addStatement("$N.$N=($T)$N.findViewById($L)", NameUtils.Variable.ANDROID_ACTIVITY, variableElement.getSimpleName(), variableElement, NameUtils.Variable.ANDROID_ACTIVITY, bindView.value()[0] ).addStatement("$N.$N.setText($N.getString($L))", NameUtils.Variable.ANDROID_ACTIVITY, variableElement.getSimpleName(), NameUtils.Variable.ANDROID_ACTIVITY, bindView.value()[1]); } } typeBuilder.addMethod(bindViewBuilder.build());
使用MethodSpec.Builder来建立bindView方法,其它的都与构造方法相似。使用returns为方法返回void类型。而后再遍历MainActivity中的注解,找到与咱们定义的BindView相匹配的字段。最后分别向bindView方法中添加findViewById与setText代码块,同时将定义的方法添加到typeBuilder中。因此执行完上面代码后在MainActivity$Binding中展现以下:
private void bindView(MainActivity activity) { activity.sName=(TextView)activity.findViewById(2131165265); activity.sName.setText(activity.getString(2131427362)); activity.sPhone=(TextView)activity.findViewById(2131165262); activity.sPhone.setText(activity.getString(2131427360)); }
实现了咱们最初的View的绑定与TextView的默认值设置。
//添加setOnClickListener成员方法 MethodSpec.Builder setOnClickListenerBuilder = MethodSpec.methodBuilder(NameUtils.Method.SET_ON_CLICK_LISTENER) .addModifiers(Modifier.PRIVATE) .returns(TypeName.VOID) .addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY, Modifier.FINAL); //添加方法内容 ClassName viewClassName = ClassName.get(NameUtils.Package.ANDROID_VIEW, NameUtils.Class.CLASS_VIEW); ClassName onClickListenerClassName = ClassName.get(NameUtils.Package.ANDROID_VIEW, NameUtils.Class.CLASS_VIEW, NameUtils.Class.CLASS_ON_CLICK_LISTENER); for (ExecutableElement executableElement : ElementFilter.methodsIn(typeElement.getEnclosedElements())) { OnClick onClick = executableElement.getAnnotation(OnClick.class); if (onClick != null) { //构建匿名class TypeSpec typeSpec = TypeSpec.anonymousClassBuilder("") .addSuperinterface(onClickListenerClassName) .addMethod(MethodSpec.methodBuilder(NameUtils.Method.ON_CLICK) .addModifiers(Modifier.PUBLIC) .addParameter(viewClassName, NameUtils.Class.CLASS_VIEW) .returns(TypeName.VOID) .addStatement("$N.$N($N)", NameUtils.Variable.ANDROID_ACTIVITY, executableElement.getSimpleName(), NameUtils.Class.CLASS_VIEW) .build()) .build(); setOnClickListenerBuilder.addStatement("$N.findViewById($L).setOnClickListener($L)", NameUtils.Variable.ANDROID_ACTIVITY, onClick.value(), typeSpec); } } typeBuilder.addMethod(setOnClickListenerBuilder.build());
与bindView方法不一样的是,因为使用到了匿名类OnClickListener与类View,因此咱们这里也要定义他们的ClassName,而后使用TypeSpec来生成匿名类。生成以后再添加到setOnClickListener方法中。最后再将setOnClickListener方法添加到MainActivity$Binding中。因此最终展现以下:
private void setOnClickListener(final MainActivity activity) { activity.findViewById(2131165265).setOnClickListener(new View.OnClickListener() { public void onClick(View View) { activity.nameClick(View); } }); activity.findViewById(2131165262).setOnClickListener(new View.OnClickListener() { public void onClick(View View) { activity.phoneClick(View); } }); }
咱们的MainActivity$Binding类就已经定义完成,最后再写入到java文件中
//写入java文件 try { JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler); } catch (IOException e) { mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement); }
在butterknife-compiler中,咱们还需建立一个特定的目录:butterknife-compiler/src/main/resources/META-INF/services
在services目录中,咱们还需建立一个文件:javax.annotation.processing.Processor
,该文件是用来告诉编译器,当它在编译代码的过程当中正处于注解处理中时,会告诉注解处理器来自动生成哪些类。
因此咱们在文件中将添加咱们自定义的Processor路径
com.idisfkj.butterknife.compiler.Processor
这样注解器就会调用该指定的Processor。到这里整个butterknife-compiler就完成了,如今咱们能够Make Project
一下工程,完成以后就能够全局搜索到MainActivity$Binding文件了。或者在以下路径中查看:
/app/build/generated/source/kapt/debug/com/idisfkj/androidapianalysis/MainActivity$Binding.java
文章中的代码均可以在Github中获取到。使用时请将分支切换到feat_annotation_processing
公众号:怪谈时间到了