赞
踩
之前一篇文章注解&反射(三)【实战–Android View IOC框架的两种实现方案】
,通过代码实践,我们一起实现了自定义的BindView框架,但是大家也发现了一个问题,之前实现的框架,只是完成了view的id与当前view的绑定,并未完成事件绑定。我们想要像成熟的ButterKnife框架一样,可以直接通过一句onClick
注解,完成事件的注册,而不是通过写不断重复的setOnClickListener
来进行事件注册。
小小的建议:本篇文章是在前几篇文章(注解&反射系列文章)基础上编写的,建议阅读此章节前,阅读之前文章。
明确一下需求的目的,我们想要在之前实现的bindview框架上,实现事件的自动注册,不用写重复的代码button.setOnClickListener(listener);
。
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show();
}
});
想要实现的效果如下:
package com.itbird.annotation.bindview.v3; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.itbird.R; import com.itbird.bindview.annotation.ItbirdAopBinderView; import com.itbird.bindview.annotation.ItbirdOnclick; /** * 编译注解+APT+JAVAPoet+反射实现 view ioc框架 * 事件自动注册 */ @ItbirdAopBinderView(R.layout.bindview_test) public class BindViewTestV3Activity extends AppCompatActivity { private static final String TAG = BindViewTestV3Activity.class.getSimpleName(); @ItbirdAopBinderView(R.id.button) public Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.bindview_test); //关键的代码,通过反射,解析AOP生成的类 BindViewImplV3.bind(this); } @ItbirdOnclick(R.id.button) public void onItbirdClick(View view) { switch (view.getId()) { case R.id.button: Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show(); break; } } }
通过目的,我们梳理了需求,我们想要通过方法注解的方式,完成控件事件的自动注册。其实,说到根本,就是想要通过方法注解,替换button.setOnClickListener(listener);
这样的一段代码。
首先我们想到的是,通过之前文章一样,像替换findViewById或者setContentView方法一样。通过编译时注解+反射+Javapoet方案,在编译时生成java代码,然后通过反射调用固定名称的方法,完成事件注册,可行吗?我们来具体分析一下。
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show();
}
});
我们把这段代码分开探索一下
1)button应该好获取,我们自定义事件注解,有viewid,然后对应当前类中找到这个viewid的变量
2)setOnClickListener方法调用,这个也没啥难度,直接用javapoet生成相关string代码,放入XXX_ViewBinding类中即可
3)Toast.makeText…具体的方法内部调用逻辑,这个就简单了,我们可以得到注解的方法,然后自己吧view参数传入我们的注解方法中,统一走id判断就可以了
经过上面分析,方案可行,接下来我们实践一下。
ItbirdOnclick
package com.itbird.bindview.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by itbird on 2022/4/19 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) //@ItbirdListenerClass(targetClass = View.OnClickListener.class, setListener = "setOnClickListener", methodName = "onClick") public @interface ItbirdOnclick { //返回注册的事件view的,id数组 int[] value(); }
书接上文,我们上一篇文章,自定义了注解处理器ItbirdAopAnnotaionProcessor
,我们在process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
方法中,拿到注解ItbirdAopBindViewAnnotation
相关的类和变量信息,然后组合成了ViewClassInfo
类,最终,我们再通过Javapoet根据ViewClassInfo类,来生成相应的java代码。
那么我们只需要在之前的ItbirdAopAnnotaionProcessor
处理类中,重要修改两处就可以了。
/**
* 指定注解处理器,可以去处理的注解类型
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(ItbirdAopBinderView.class.getCanonicalName());
set.add(ItbirdOnclick.class.getCanonicalName());
return set;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 日志打印,用于辅助process方法是否被执行
mMessager.printMessage(Diagnostic.Kind.NOTE, "---------------ItbirdAopAnnotaionProcessor start------------");
//处理所有的 ItbirdAopBindViewAnnotation 注解
handleItbirdAopBindViewAnnotation(roundEnvironment);
//处理所有的 ItbirdOnclick 注解
handleItbirdOnclickAnnotation(roundEnvironment);
//使用javapoet生成java文件
createJavaFile();
return roundEnvironment.processingOver();
}
看到这里,我们在process中加入了函数handleItbirdOnclickAnnotation
/** * 处理所有的 ItbirdOnclick 注解 * * @param roundEnvironment */ private void handleItbirdOnclickAnnotation(RoundEnvironment roundEnvironment) { Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ItbirdOnclick.class); // 遍历拥有该注解的元素 for (Element element : elements) { mMessager.printMessage(Diagnostic.Kind.NOTE, "element = " + element.toString()); //这里由于OnClick注解只能标注在Method上,所以直接转为ExecutableElement ExecutableElement executableElement = (ExecutableElement) element; //首先得到类节点 TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement(); ViewClassInfo viewClassInfo = getViewClassInfo(typeElement); viewClassInfo.addMethodElement(executableElement); } }
是不是很简单,就是找到这个注解相关的方法元素(ExecutableElement ),然后把它归整到相应的ViewClassInfo
中。由之前的文章知道,我们框架中的ViewClassInfo
类的定义是,将一个类中相关的注解信息(方法注解、属性注解、类注解等)统一到一个类中,以方便后面通过javapoet,遍历这个类信息,来生成对应的java类。
我们看到上面函数process
中的代码,在加载完注解信息之后,会调用javapoet去根据注解信息生成java文件
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 日志打印,用于辅助process方法是否被执行
mMessager.printMessage(Diagnostic.Kind.NOTE, "---------------ItbirdAopAnnotaionProcessor start------------");
//处理所有的 ItbirdAopBindViewAnnotation 注解
handleItbirdAopBindViewAnnotation(roundEnvironment);
//处理所有的 ItbirdOnclick 注解
handleItbirdOnclickAnnotation(roundEnvironment);
//使用javapoet生成java文件
createJavaFile();
return roundEnvironment.processingOver();
}
接下来,主要看createJavaFile
了,也就是如何通过javapoet来生成java文件。
首先我们搞清楚目标,我们是想通过javapoet生成怎样的代码,大家也好在脑海中有个印象,我们想通过javapoet,根据刚刚得到的注解信息,生成如下的java代码文件。
package com.itbird.annotation.bindview.v3;
public class BindViewTestV3Activity_ViewBinding {
public static void bind(BindViewTestV3Activity activity) {
activity.setContentView(2131427358);
activity.button = activity.findViewById(2131230809);
activity.button.setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
activity.onItbirdClick(v);
}
});
}
}
接下来我们一步一步来。
通过刚刚处理注解信息,我们把注解归类到了统一的ViewClassInfo类中,每个ViewClassInfo又包含在了map中,那么就是代表我们要生成多少个java文件了。
/**
* 使用javapoet,遍历map,生成java文件
*/
private void createJavaFile() {
mMessager.printMessage(Diagnostic.Kind.NOTE, "createJavaFile");
mMessager.printMessage(Diagnostic.Kind.NOTE, "createJavaFile map = " + map.toString());
for (String classname : map.keySet()) {
ViewClassInfo proxy = map.get(classname);
if (proxy != null) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "createJavaFile " + proxy.getClassFullName());
proxy.createJavaCode(mProcessingEnvironment, mElementUtils);
}
}
}
public void createJavaCode(ProcessingEnvironment mProcessingEnvironment, Elements elementUtils) { //获取类信息 ClassName className = ClassName.get(typeElement); //构建bind的入参 ParameterSpec parameterSpec = ParameterSpec.builder(className, BIND_METHOD_PARAMETER_NAME).build(); MethodSpec methodSpec = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.STATIC, Modifier.PUBLIC) .addParameter(parameterSpec) .addCode(generateJavaCode()) .build(); //构造类 TypeSpec typeSpec = TypeSpec.classBuilder(getClassSimpleName() + "_ViewBinding") .addModifiers(Modifier.PUBLIC) .addMethod(methodSpec) .build(); //创建文件 JavaFile javaFile = JavaFile.builder(getPackageName(elementUtils), typeSpec). build(); //写入文件 try { javaFile.writeTo(mProcessingEnvironment.getFiler()); } catch (IOException e) { e.printStackTrace(); } }
从这处代码看,很简单,利用javapoet的一些api,来生成相应的类、方法、创建java文件。但是我们也知道,构建外围的类、方法名什么的,使用这些api很简单就能做到,但是关键的bind内部的方法实现呢?
构建了bind方法,bind方法有入参activity,然后通过generateJavaCode
方法,构建了bind方法内部的代码,也就是上面的关键代码
activity.setContentView(2131427358);
activity.button = activity.findViewById(2131230809);
activity.button.setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(android.view.View v) {
activity.onItbirdClick(v);
}
});
我们来看一下generateJavaCode
方法
/** * 创建java代码 * * @return */ private CodeBlock generateJavaCode() { CodeBlock.Builder codeBlock = CodeBlock.builder(); /** * activity.setContentView( 2131427358 ); */ //setContentView方法生成 if (typeElement != null) { codeBlock.add(BIND_METHOD_PARAMETER_NAME + ".setContentView( $L );\n", getItbirdAopBinderViewAnnotationValue(typeElement)); } /** * activity.textView1 = activity.findViewById( 2131231131 ); */ //findViewById方法生成 if (variableElements != null) { for (VariableElement element : variableElements) { codeBlock.add(BIND_METHOD_PARAMETER_NAME + "." + element.getSimpleName() + " = " + BIND_METHOD_PARAMETER_NAME + ".findViewById( $L );\n", getItbirdAopBinderViewAnnotationValue(element)); } } /** * activity.button.setOnClickListener(new android.view.View OnClickListener() { * @Override * public void onClick(android.view.View v) { * activity.onItbirdClick(v); * } * }); */ //setOnClickListener方法生成 //TODO 这儿有个问题,是通过遍历view属性,去找到view控件,从而通过字符串形式去设置的onclick事件,如果view没有使用注解,则得不到这个view,导致方法注册事件 if (methodElements != null) { for (ExecutableElement element : methodElements) { int[] ids = element.getAnnotation(ItbirdOnclick.class).value(); for (int id : ids) { for (VariableElement variableElement : variableElements) { if (getItbirdAopBinderViewAnnotationValue(variableElement) == id) { //TODO 这儿暂时都是以直接写死的字符串,来直接生成的代码,第二版本,可以考虑,通过注解优化适配 //TODO FOR循环优化 codeBlock.add(BIND_METHOD_PARAMETER_NAME + "." + variableElement.getSimpleName() + ".setOnClickListener(new android.view.View.OnClickListener() {\n" + "@Override\n" + "public void onClick(android.view.View v) {\n" + BIND_METHOD_PARAMETER_NAME + "." + element.getSimpleName() + "(v);\n" + "}\n" + " });\n"); break; } } } } } return codeBlock.build(); }
其实大家一看,也很简单,就是通过遍历刚刚得到的注解信息,然后通过字符串拼接形式,生成javacode,这里有一个注意点,本Demo样例中,我们的BindviewAnnotationProcessor
和BindviewAnnotation
两个Module都是java libary,自然而然无法在类中,自动import android.view.View
,所以这里直接在代码中,通过全路径(android.view.View.OnClickListener类似的形式)
去string写入l了javacode。
package com.itbird.annotation.bindview.v3; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.itbird.R; import com.itbird.bindview.annotation.ItbirdAopBinderView; import com.itbird.bindview.annotation.ItbirdOnclick; /** * 编译注解+APT+JAVAPoet+反射实现 view ioc框架 * 事件自动注册 */ @ItbirdAopBinderView(R.layout.bindview_test) public class BindViewTestV3Activity extends AppCompatActivity { private static final String TAG = BindViewTestV3Activity.class.getSimpleName(); @ItbirdAopBinderView(R.id.button) public Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //关键的代码,通过反射,解析APT生成的类 BindViewImplV3.bind(this); } @ItbirdOnclick(R.id.button) public void onItbirdClick(View view) { switch (view.getId()) { case R.id.button: Toast.makeText(this, "button on click", Toast.LENGTH_SHORT).show(); break; } } }
BindViewImplV3 .java·
package com.itbird.annotation.bindview.v3; import android.app.Activity; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * 反射调用APT+javapoet生成的类 * Created by itbird on 2022/4/11 */ public class BindViewImplV3 { 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(); } } }
我们发现在zapp的build目录下,生成了对应的java文件
同时,点击运行一下,发现Toast正常展示
我们通过编译时注解处理(APT)+JavaPoet(生成java代码)+反射(调用bind方法),来在框架中实现了,事件的自动绑定和视图的自动绑定。
写到这里,框架的确已经完成了预定的目标,但是依然有不足之处,这里做出总结,以便于后续持续优化
1)方法注解的处理,有个问题,是通过遍历view属性,去找到view控件,从而通过字符串形式去设置的onclick事件,如果view没有使用注解,则得不到这个view,导致方法注册事件
2)方法内部的代码实现,暂时都是以直接写死的字符串,来直接生成的代码,第二版本,可以考虑,通过注解优化适配
3)目前ItbirdOnclick只支持setOnClickListener事件的自动绑定,这个是因为代码都是通过string形式的拼接来完成生成的,后续可以通过注解的注解,来对ItbirdOnclick进行扩展完成更多事件的绑定功能
我们通过在gradle中导入ButterKnife相关包之后,来看一下
implementation 'com.jakewharton:butterknife:10.2.1'
//如果您使用的是kotlin,请使用kapt
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
不出所料,果然和我们实现的框架一样,也是使用了APT,然后编译时处理注解,生成了XXX_ViewBinding
类,然后在运行时,通过反射去加载XXX_ViewBinding类中的相关信息。
简单总结一下:
ButterKnife使用注解解析器(Annotation Processor Tool),在编译期对使用了各种注解的类进行解析,然后使用Square提供的JavaPoet开源工具生成了一个java文件XXX_ViewBinding.java,然后在BindView.bind(this)的时候,通过反射找到XXX_ViewBinding这个类中对应的那个构造器对象,并以当前XXX为键,找到的这个XXX_ViewBinding对应的那个构造器对象为值,添加到了一个LinkedHashMap中,作为缓存。通过这个构造器对象调用构造方法,完事。其实findViewById的操作是被转移到了这个XXX_ViewBinding的构造方法中。
当然话虽如此,但是我们也知道,ButterKnife内部如何根据注解生成java代码这块,还是值得我们深入学习研究一下,各位有兴趣可以看一下,当然,小编日后有时间,也会出文章来深入探讨一下。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。