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