注解框架Butterknife解析
1.什么是注解
2.注解的分类
3.编译时注解的原理
4.APT
5.创建项目及依赖
6.编码实现
7.总结
我们首先了解一下什么是注解以及注解的核心原理,在掌握原理的前提下自己动手实现一个注解框架。通过代码的编写能够对Butterknife底层实现有更加清楚的认识。
注解
注解在Java文档中定义如下:
An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.
翻译一下,大概的意思是:
注解是一种元数据, 可以添加到java代码中. 类、方法、变量、参数、包都可以被注解,注解对注解的代码没有直接影响。
注解的分类
- <b>运行时注解:</b>指的是运行阶段利用反射,动态获取被标记的方法、变量等,如EvenBus。
- <b>编译时注解:</b>指的是程序在编译阶段会根据注解进行一些额外的处理,如ButterKnife。
运行时注解和编译时注解,都可以理解为通过注解标识,然后进行相应处理。两者的区别是:前者是运行时执行的,反射的使用会降低性能;后者是编译阶段执行的,通过生成辅助类实现效果。
运行时注解由于性能问题被一些人所诟病,所以本文主要讲解编译时注解的原理,并实现自己的Butterknife框架。
编译时注解的原理
编译时注解的核心原理依赖APT(Annotation Processing Tools)实现:
编译时Annotation解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是ButterKnife Dragger等开源库的基本原理
那么APT又是什么呢?
APT(Annotation Processing Tool)是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
下面以Butterknife为例:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_main)
TextView tvMain;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
字节码文件这是Butterknife最简单的用法,我们只需要加上一个注解 @BindView并指定对应的Id就可以了,从而避免了findViewById(),那么它底层是怎么实现的呢?本篇文章重点不是介绍Butterknife的实现原理,所以这里只是简单的说一下它底层的实现。这里我们只写了一个MainActivity.java文件,编译后我们查看一下class文件,我们会发现在MainActivity中多了一个内部类ViewBinder,其实Butterknife就是在这个内部类中关联对应控件的,下面以伪代码的形式简单说明一个它底层实现的原理。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_main)
TextView tvMain;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
/*
* 当bind()方法被调用之后,tvMain就有对应的值了
* */
private static class ViewBinder{
public static void bind(MainActivity activity){
activity.tvMain = (TextView) activity.findViewById(R.id.tv_main);
}
}
}
大概的原理是这样的:在编译的时候如果某个类中使用了注解,Butterknife就会在其中“添加”一个内部类,在内部类中实现控件的关联。我们知道编译java源文件的工具是javac,其实在javac中有一个注解处理工具(依赖APT)用来编译时扫描和处理的注解的工具。我们可以为特定的注解,注册你自己的注解处理器,来实现自己的处理逻辑。
上面我们了解了基本原理,接下来我们实战演练
项目结构
我们的项目结构如上图所示:每个库都有自己的实现功能,最中通过我们的项目依赖相应的库来使用。
创建App
我们新建一个工程,因为我们要处理注解需要用到APT,所以在app中需要使用apt的插件
<b>github:</b>https://github.com/Aexyn/android-apt
关联APT插件:
Step1: 在我们工程目录下的build.gradle文件中添加如下代码:
Step2: 在我们项目目录下的build.gradle文件中添加如下代码:
创建Java库(定义注解)
inject-annotion创建Android库
inject<b>关联Java库(inject-annotion)</b>
关联java库创建Java库(处理注解库)
我们需要在编译的时候根据注解创建新的类并添加到源文件中,所有需要引用几个依赖。并且要关联上一个Java库
- com.google.auto.service:auto-service:谷歌提供的Java 生成源代码库
- com.squareup:javapoet:提供了各种 API 让你用各种姿势去生成 Java 代码文件
- com.google.auto:auto-common:生成代码的库
<b>全部创建完毕后,我们的工程目录如下:</b>
项目目录关联库
让我们的项目(app)去关联注解库
关联库编写代码
1.定义注解:inject-annotion
/**
* @Retention(RetentionPolicy.CLASS):编译时被保留,在class文件中存在,但JVM将会忽略
* @Target(ElementType.FIELD) :出现的位置(字段、枚举的常量)
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
2.定义方法:inject
-
<b>InjectView.java</b>
public class InjectView { public static void bind(Activity activity) { String clsName=activity.getClass().getName(); try { //获取内部类 Class<?> viewBidClass=Class.forName(clsName+"$$ViewBinder"); //创建内部类的实例 ViewBinder viewBinder= (ViewBinder) viewBidClass.newInstance(); viewBinder.bind(activity);//绑定页面 } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
-
<b>ViewBinder.java</b>
public interface ViewBinder <T>{ void bind(T tartget); }
3.处理注解:inject-compiler
-
<b>FieldViewBinding.java</b>
/** * 注解信息封装类 */ public class FieldViewBinding { private String name;// 字段的名字 textview private TypeMirror type ;// 字段的类型 --->TextView private int resId;// 对应的id R.id.textiew public FieldViewBinding(String name, TypeMirror type, int resId) { this.name = name; this.type = type; this.resId = resId; } public String getName() { return name; } public TypeMirror getType() { return type; } public int getResId() { return resId; } }
-
<b>BindViewProcessor.java</b>
/** * 注解处理类 */ @AutoService(Processor.class) public class BindViewProcessor extends AbstractProcessor { private Elements elementUtils; private Types typeUtils; private Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); elementUtils = processingEnvironment.getElementUtils(); typeUtils = processingEnvironment.getTypeUtils(); filer = processingEnvironment.getFiler(); } /* 设置处理那些注解 */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); return types; } /* 设置支持的JDk版本 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>(); for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) { TypeElement enClosingElement = (TypeElement) element.getEnclosingElement(); List<FieldViewBinding> list = targetMap.get(enClosingElement); if (list == null) { list = new ArrayList<>(); targetMap.put(enClosingElement, list); } int id = element.getAnnotation(BindView.class).value(); String fieldName = element.getSimpleName().toString(); TypeMirror typeMirror = element.asType(); FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id); list.add(fieldViewBinding); } for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) { List<FieldViewBinding> list = item.getValue(); if (list == null || list.size() == 0) { continue; } TypeElement enClosingElement = item.getKey(); String packageName = getPackageName(enClosingElement); String complite = getClassName(enClosingElement, packageName); ClassName className = ClassName.bestGuess(complite); ClassName viewBinder = ClassName.get("com.example.inject", "ViewBinder"); TypeSpec.Builder result = TypeSpec.classBuilder(complite + "$$ViewBinder") .addModifiers(Modifier.PUBLIC) .addTypeVariable(TypeVariableName.get("T", className)) .addSuperinterface(ParameterizedTypeName.get(viewBinder, className)); MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC) .returns(TypeName.VOID) .addAnnotation(Override.class) .addParameter(className, "target", Modifier.FINAL); for (int i = 0; i < list.size(); i++) { FieldViewBinding fieldViewBinding = list.get(i); String pacckageNameString = fieldViewBinding.getType().toString(); ClassName viewClass = ClassName.bestGuess(pacckageNameString); methodBuilder.addStatement ("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName() , viewClass, fieldViewBinding.getResId()); } result.addMethod(methodBuilder.build()); try { JavaFile.builder(packageName, result.build()) .addFileComment("auto create make") .build().writeTo(filer); } catch (IOException e) { e.printStackTrace(); } } return false; } /* 获取类名 */ private String getClassName(TypeElement enClosingElement, String packageName) { int packageLength = packageName.length() + 1; return enClosingElement.getQualifiedName().toString().substring(packageLength).replace(".", "$"); } /* 获取包名 */ private String getPackageName(TypeElement enClosingElement) { return elementUtils.getPackageOf(enClosingElement).getQualifiedName().toString(); } }
测试
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text)
TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectView.bind(this);
if(textview == null){
Toast.makeText(this,"注解处理失败",Toast.LENGTH_SHORT).show();
}else{
textview.setText("世界你好!");
}
}
}
演示用法跟Butterknife一样,页面上有一个TextView,使用注解关联,如果关联失败,弹出提示信息。否则设置显示为“世界你好!”。
总结
通过上述代码的编写,我们能更加对Butterknife的底层实现有更清楚的认识,虽然只是实现了绑定View。在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,我们需要继承该类重写此方法我们就能获取我们想要处理的注解。在里面做具体的绑定逻辑。
AutoService注解处理器是Google开发的,用来生成META-INF/services/javax.annotation.processing.Processor文件的。我们可以在注解处理器中使用注解。非常方便。