曾经看过一篇使用运行时注解来实现类似 ButterKnife 功能的文章。直到后来我自己看了ButterKnife 源码后才发现并不是这样。推荐阅读 ButterKnife 原理解析,这是 Butterknife 源码地址,不妨 clone 下来看一看瞧一瞧。
代码地址:android-annotation-tutorial
反射是一个我们在运行时读取一个类及其成员属性,并尝试修改这些属性的过程。 这个过程虽然有助于创建一个通用或独立于实现的程序,但是由于我们不知道运行时的确切条件,因此也容易出现大量异常。 通过反射进行类扫描和修改是一个缓慢的过程,也是一种孤立代码的丑陋方式。
一、示例:
为了更好的理解编译时注解,我们先使用运行时注解来实现绑定控件
-
定义注释BindView以进行映射
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
-
将BindView注释放在具有视图ID的View类变量上
public class MainActivity extends AppCompatActivity {
...
@BindView(R.id.txtView)
TextView txtView;
...
}
-
创建一个类,该类使用id tv_name将XML中定义的TextView对象赋值给变量tvName
public class ViewBinder {
/*
* annotations for activity class
* @param target is the activity with annotations
*/
public static void bind(final Activity target){
bindViews(target, target.getClass().getDeclaredFields(),
}
/*
* initiate the view for the annotated public fields
* @param obj is any class instance with annotations
* @param fields list of methods in the class with annotation
* @param rootView is the inflated view from the XML
*/
private static void bindViews(final Object obj, Field[] fields, View rootView){
for(final Field field : fields) {
Annotation annotation = field.getAnnotation(BindView.class);
if (annotation != null) {
BindView bindView = (BindView) annotation;
int id = bindView.value();
View view = rootView.findViewById(id);
try {
field.setAccessible(true);
field.set(obj, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
-
将Activity实例发送到ViewBinder。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.txtView)
private TextView txtView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);
txtView.setText("Testing");
}
...
}
这种方法能正常运行,但我们刚谈到反射的性能限制,使它变得不可取。
那么,我们怎样才能改进呢?
- 我们必须消除MainActivity类的运行时扫描并将其替换为方法调用。
- 我们不希望为每个Activity编写这些方法,并希望它们自动生成。
- 我们希望消除任何运行时异常,并希望在编译期间移动此类检查。
编译时注解能满足这些情况。
二、编译时注解如何工作?
编译时注解在编译周期中进行。 在每个循环遍历中,编译器在读取java源文件时找到注册用于处理的注释并调用相应的注释处理器。 如果在该循环中没有生成文件,则该循环继续生成任何文件或终止。
好吧。我们将学习过程分为四个部分:
- 为注释处理创建一个Android项目。
- 理解用于处理的注释的定义。
- 编写一个编译器模块,通过注释处理生成代码。
- 使用通过注释处理生成的代码。
三、项目结构
该项目有四个模块:
- app:这是Android应用程序项目。
- binder:此模块提供一个类,该类将给Activity带注释的视图对象和单击回调方法映射到XML视图。
- binder-annotations:此模块定义注释以便于视图和单击回调方法的映射。
- binder-compiler:此模块定义生成类以帮助上述映射的处理器。
-
定义注释
BindView:它将视图引用映射到其XML定义。 示例:带有id tv_content的TextView将映射到变量tvContent。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value();
}
OnClick:它将映射一个方法,当单击具有提供的id的视图时将调用该方法。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface OnClick {
@IdRes int value();
}
在这里你可以注意到@IdRes注释。 此注释由support-annotations库提供。
-
创建注释处理器
方法一:
注释处理器循环运行并与应用程序编译并行运行。 在每个周期中,处理器都提供有关正在编译的应用程序源代码的信息。
处理器必须注册到编译器,以便在编译应用程序时可以运行它。 我们将看到如何定义这样的编译器。
现在,我们将创建一个类似于binder-annotations的Java库binder-compiler。 在这个模块中,我们将不得不创建目录结构:
binder-compiler/src/main/resources/META-INF/services
在services目录中,我们将创建一个名为javax.annotation.processing.Processor的文件。
此文件将列出编译器在注释处理时编译应用程序源代码时将调用的类。
还有一种方法是(我采用了这种):
使用 Google 提供的库 auto-service ,gradle 添加依赖
implementation 'com.google.auto.service:auto-service:1.0-rc2'
Processor 添加如下注解
@AutoService(javax.annotation.processing.Processor.class)
public class Processor extends AbstractProcessor {
...
}
所有注释处理器都继承AbstractProcessor,它定义了处理的基本方法。 我们将在此库中创建一个继承AbstractProcessor的类Processor。 我们必须覆盖三种方法来提供处理的实现。
- init:这里我们将获得Filer,Messager和Elements。
- process:调用此方法来处理应用程序的源代码。 在这里,我们将定义一个类并编写Java源代码。
- getSupportedAnnotationTypes:它列出了我们在处理应用程序的Java文件时要查询的注释。
另外还要了解如下类:
- Filer:它提供API来编写生成的源代码文件。
- Messager:用于在编译时打印消息。 我们发送可能通过Messager处理的错误消息。 由于注释处理器在其自己的独立环境中运行,因此我们无法通过任何其他方式与应用程序通信。
- Elements:它提供了utils方法,用于过滤处理器中不同类型的元素。
public class Processor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// all the magic happens in this block
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return new TreeSet<>(Arrays.asList(
BindView.class.getCanonicalName(),
OnClick.class.getCanonicalName()
));
}
}
四、 生成 Java 源代码
Note: 如果有些方法不明白可以参照 Android Studio中调试自定义AbstractProcessor方法 把程序跑起来,打断点看下就一目了然了。
现在我们提供Processor的流程方法的完整实现,并学习使用JavaPoet定义类及其成员。
注释处理在处理Java注释源代码时提供的内容:
- Set<? extends TypeElement>:它提供注释列表作为正在处理的Java文件中包含的元素。
- RoundEnvironment:它提供对处理环境的访问,其中包含查询元素的工具。 我们将在这个环境中使用的两个主要功能是:processingOver(它的最后一轮处理)和getRootElements(它提供了一个将被处理的元素列表。这些元素中将包含一些我们感兴趣的注释。)
因此,我们有一组注释和一系列元素。 我们的库将生成一个包装类,它将帮助映射视图并单击活动的侦听器。
我们的注释将映射视图和按钮以删除样板,就像ButterKnife一样。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_content)
TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Binding.bind(this);
}
@OnClick(R.id.bt_1)
void bt1Click(View v) {
tvContent.setText("Button 1 Clicked");
}
@OnClick(R.id.bt_2)
void bt2Click(View v) {
tvContent.setText("Button 2 Clicked");
}
}
我们将使用MainActivity定义使用注释处理自动生成名为MainActivity$Binding的包装器类。处理后,将创建以下类。编译的时候生成在该目录下:
[图片上传失败...(image-9b18a6-1536888483860)]
打开后看到如下内容:
public class MainActivity$Binding {
public MainActivity$Binding(MainActivity activity) {
bindViews(activity);
bindOnClicks(activity);
}
private void bindViews(MainActivity activity) {
activity.tvContent = (TextView)activity.findViewById(2131165322);
}
private void bindOnClicks(final MainActivity activity) {
activity.findViewById(2131165218).setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
activity.bt1Click(view);
}
});
activity.findViewById(2131165219).setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
activity.bt2Click(view);
}
});
}
}
现在我们知道了我们必须生成什么,让我们分析如何使用我们在处理时可以使用的信息来创建它。
我们将首先过滤掉那些在getRootElements方法提供的元素列表中使用@BindView或@OnClick的类(Type)元素。
然后,我们将迭代这些过滤的元素,然后扫描其成员和方法,以使用JavaPoet开发包装类的类模式。 最后,我们将该类写入Java文件。希望使搜索更有效, 因此,我们将创建一个具有过滤方法的ProcessingUtils类。
public class ProcessingUtils {
private ProcessingUtils() {
// not to be instantiated in public
}
public static Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
Set<? extends Element> supportedAnnotations) {
Set<TypeElement> typeElements = new HashSet<>();
for (Element element : elements) {
if (element instanceof TypeElement) {
boolean found = false;
for (Element subElement : element.getEnclosedElements()) {
for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
for (Element annotation : supportedAnnotations) {
if (mirror.getAnnotationType().asElement().equals(annotation)) {
typeElements.add((TypeElement) element);
found = true;
break;
}
}
if (found) break;
}
if (found) break;
}
}
}
return typeElements;
}
}
这里有两件事需要我们理解:
- element.getEnclosedElements():封闭元素是给定元素中包含的元素。 在我们的例子中,元素将是MainActivity(TypeElement),Enclosed元素将是tvContent,onCreate,bt1Click,bt2Click和其他继承的成员。
- subElement.getAnnotationMirrors():它将提供subElement上使用的所有注释。
JavaPoet速成课程:
JavaPoet使得定义类结构并在处理时编写它非常简单。 它创建了非常接近手写代码的类。 它提供了自动推断导入以及美化代码的工具。当然也可以使用 JavaFileObject ,但是这个远古的笨重且不切实际的东西我们就不多说了,有兴趣的还是建议放弃这个兴趣,就使用 JavaPoet 吧。
要使用JavaPoet,我们需要将以下依赖项添加到binder-compiler模块中。
dependencies {
implementation project(':binder-annotations')
implementation 'com.squareup:javapoet:1.11.1'
}
本教程所需的JavaPoet的基本用法(可以从其 javapoet 获得任何提前了解。)
- TypeSpec.Builder:定义类模式。
- addModifiers(Modifier):添加private,public或protected关键字。
- addAnnotation:向元素添加注释。 示例:@Override on methods或@Keep on class in case。
- TypeSpec.Builder - > addMethod:向类添加方法。 示例:构造函数或其他方法。
- MethodSpec - > addParameter:为方法添加参数类型及其名称。 示例:在我们的示例中,我们希望将具有变量名称活动的MainActivity类型传递给方法。
- MethodSpec - > addStatement:它将在方法中添加代码块。 在这个方法中,我们首先定义语句的占位符,然后传递参数来映射那些占位符。 示例
addStatement("$N($N)", "bindViews", "activity") //这将生成代码bindViews(activity)
占位符:T -> type(ClassName), $L -> literals(long etc.)
参考JavaPoet的基本介绍,可以很容易地理解其他内容。剩下的就是你自己去完成了。
「百闻不如一见」,百看不如一试。最后再说下源码地址吧:
android-annotation-tutorial
参考链接: