手拉手系列之造一个简易的ButterKnife学习编译时注解
注解对于我们工作和学习来说是非常重要的,我们平时开发中常用到的一些第三方框架中到处都有注解的影子,我们平时一般写运行时注解相对多些,运行时注解信息会在JVM中保留,因此我们可以通过反射来拿到注解信息,说到反射那么性能上就会相对有所损耗,其实反射造成的性能损耗没有想象中的那么可怕,用到反射时我们一般是配合着缓存来做的,这样做的好处当然是尽可能的减少性能上的损耗咯。像我们熟悉的ButterKnife框架,他采用的是编译时注解的方式(也就是RetentionPolicy.CLASS),这样做的好处就是会在程序编译期间利用注解处理器生成我们需要的代码,程序运行时就可以直接从代码中获取信息。这种方式产生的影响就是项目编译的时间会长些(因为要在编译的时候解析我们的注解信息生成我们需要的代码),那么程序在运行时就可以直接使用已经生成的代码,所以就不存在性能上的问题了。关于注解相关的知识点已经有前辈总结的非常好了,这里就不再啰嗦了,需要的话可以点击此链接直接阅读深入理解Java注解类型(@Annotation)
这里我们以造轮子的方式来学习编译时注解,即手拉手实现一个简易版的ButterKnife,实现的功能有findViewById、点击事件onClickListener以及页面销毁时解除绑定等。我们先把最终如何在项目中使用的代码贴出来
public class MainActivity extends AppCompatActivity {@BindView(R.id.tv_annotate)
TextView tvAnnotate;
@BindView(R.id.bt_annotate_click)
Button btn;
@BindView(R.id.bt_annotate_click2)
Button btn2;
@BindView(R.id.bt_annotate_click3)
Button btn3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyKnife.inject(this);
tvAnnotate.setText("自定义注解实现简易的ButterKnife效果");
btn.setText("注解实现点击");
}@OnClick(ids = {R.id.bt_annotate_click, R.id.bt_annotate_click2})
void btnClick(View view){
switch (view.getId()){
case R.id.bt_annotate_click:
btn.setText("实现了点击效果");
Toast.makeText(this, "实现了点击效果", Toast.LENGTH_SHORT).show();
break;
case R.id.bt_annotate_click2:
btn2.setText("第二个按钮被点击了哦");
Toast.makeText(this, "第二个按钮被点击了哦", Toast.LENGTH_SHORT).show();
break;
}
}@OnClick(ids = R.id.bt_annotate_click3)
void btn3Click(){
btn3.setText("我被点击了哦");
Toast.makeText(this, "我被点击了哦", Toast.LENGTH_SHORT).show();
}@Override
protected void onDestroy() {
super.onDestroy();
MyKnife.unBind(this);
}
}
为了方便大家理解,这里先提前把最后生成的代码文件贴出来,这样大家就知道了我们后续的一系列操作的目的就是为了在项目编译时能生成这个代码源文件,如下图MainActivity$$Injector类
package com.ch.bk;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.ch.api.interfaces.Injector;
import com.ch.api.provider.ProviderI;
import java.lang.Object;
import java.lang.Override;
public class MainActivity$$Injector implements Injector {
private View view2131165218;
private View view2131165219;
private View view2131165220;
@Override
public void inject(final MainActivity host, Object source, ProviderI provider) {
host.tvAnnotate = (TextView) provider.findViewById(source, 2131165324);
host.btn = (Button) provider.findViewById(source, 2131165218);
host.btn2 = (Button) provider.findViewById(source, 2131165219);
host.btn3 = (Button) provider.findViewById(source, 2131165220);
View.OnClickListener listener;
listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
host.btnClick(view);
}
};
view2131165218 = provider.findViewById(source, 2131165218);
view2131165218.setOnClickListener(listener);
view2131165219 = provider.findViewById(source, 2131165219);
view2131165219.setOnClickListener(listener);
listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
host.btn3Click();
}
};
view2131165220 = provider.findViewById(source, 2131165220);
view2131165220.setOnClickListener(listener);
}@Override
public void unBind(MainActivity host) {
host.tvAnnotate = null;
host.btn = null;
host.btn2 = null;
host.btn3 = null;
view2131165218.setOnClickListener(null);
view2131165218 = null;
view2131165219.setOnClickListener(null);
view2131165219 = null;
view2131165220.setOnClickListener(null);
view2131165220 = null;
}
}
知道了怎么回事之后,接下来我们就要按部就班的去一步步做准备工作了。
首先按照ButterKnife的代码结构来规划我们的项目结构,ButterKnife官网的主要结构是这样的:
butterknife-annotations:这是个java library 管理着我们的自定义注解可能到这里大家会有些疑惑,为什么有的是java library有的是android library呢?这里简单解释下,我们写注解处理器时需要继承AbstractProcessor重写他的process方法,看下AbstractProcessor类的包名javax.annotation.processing.AbstractProcessor,javax包系列属于java,在android核心库中没有。而像butterknife这个提供开发者调用的api这是个android library,这是因为我们是在android项目中去直接调用此api进行绑定等操作。
butterknife-compiler:这也是个java library 这是用来处理我们自定义注解的注解处理器
butterknife:这是个android library 里面是提供开发者调用的api,比如绑定、解绑等
在开始之前为了便于理解,我们先把整个项目的结构展示一下
文章图片
annotation_structure.png 自定义注解 就是位于上图中myknife-annotations这个module中
注解一:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}注解二:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
int[] ids();
}
这里有几点细节需要说明下,①我们可以设置默认值,如果有需要的话 ②比如我们的第一个注解定义了名为value的元素,这样做的好处是:如果该元素是唯一需要赋值的一个元素,那么就不需要key=value这种方式了,直接在括号里写对应的值就行了,前提是这个元素名必须写成value,关于注解其他的知识点不熟悉的话可以查看文章开头我推荐的前辈博客,总结的很详细。
创建绑定等操作的类(提供给开发者使用的入口) 在文章开头我们贴出来了如何使用的代码,从代码中可以看到,我们在MainActivity的onCreate方法中直接调用了MyKnife.inject(this) 进行了绑定,在onDestroy方法中调用MyKnife.unBind(this) 进行解绑,那么你心中有没有一种疑惑,为什么几个注解以及短短的一句绑定代码就能帮我们省去findViewById操作?就能帮我们省去在类名后实现onclicklistener监听,然后重写onClick方法等一系列反复的操作?接下来就看看这个inject方法,其实阅读源码也是一样,我们找到入口之后进去查看(扯远了~~)
/**
* 提供初始化入口类,编译时生成代码文件
*/
public class MyKnife {
private static final ActivityProvider ACTIVITY_PROVIDER = new ActivityProvider();
private static final ViewProvider VIEW_PROVIDER = new ViewProvider();
private static final Map INJECTOR_MAP = new HashMap<>();
/** activity页面绑定入口 **/
public static void inject(Activity activity){
inject(activity, activity, ACTIVITY_PROVIDER);
}/** view页面绑定入口 **/
public static void inject(View view){
inject(view, view, VIEW_PROVIDER);
}/** fragment页面绑定入口 **/
public static void inject(Object host, View view){
inject(host, view, VIEW_PROVIDER);
}private static void inject(Object host, Object source, ProviderI provider){
//通过反射获取类名
String className = host.getClass().getName();
//先从缓存中获取值
Injector injector = INJECTOR_MAP.get(className);
try {
//为空则实例化一个
if (injector == null) {
//反射,通过类名的全路径获取类对象
Class> clazz = Class.forName(className + "$$Injector");
injector = (Injector) clazz.newInstance();
}
//这里用接口的好处是:如果不使用接口的话,我们需要循环遍历这个自动生成的clazz类里面定义的所有方法,找到我们需要的方法然后给此方法赋值
//使用接口的话只需要生成的类去实现此接口,可以提升效率,减少性能损耗
injector.inject(host, source, provider);
} catch (Exception e) {
e.printStackTrace();
}}/** 解除绑定 **/
public static void unBind(Object host){
String clazzName = host.getClass().getName();
Injector injector = INJECTOR_MAP.get(clazzName);
if (injector != null) {
injector.unBind(host);
}
}
}
说明:ActivityProvider类的主要核心作用就是调用当前activity的findViewById方法,注意这里是activity的,那么如果我们想在fragment或者其他类(比如adapter适配器)中使用怎么办呢,这一类的就需要调用View的findViewById了,这就是ViewProvider的主要作用。
前面说了程序在编译期会去调用我们的注解处理器中的process去处理(后面会讲到自定义注解处理器)生成我们需要的.java源文件,程序运行时需要获取这个文件中的内容的话就直接从这个文件中获取即可,所以我们需要通过反射去找到这个生成的类文件,反射肯定会有损性能的,所以这里我们用HashMap缓存的方式去降低这种性能的损耗。这里还有一个小优化就是injector.inject(host, source, provider); 通过接口的方式调用,关于这样做的好处在代码中已经加以注释了。
注解处理器 注解定义完了,剩下的就是如何让程序在编译期去识别解析我们定义的注解,并生成代码在程序运行时提供给程序用?这是最难的,那么我们应该怎么去自定义注解处理器?不要慌java已经给我们提供了这样的类AbstractProcessor。我们继承这个AbstractProcessor并重写他的process就可以了,这是因为编译器在编译时会自动查找所有继承AbstractProcessor的类,并调用他的process方法去处理。
光这些还不行,我们创建了注解处理器之后需要配置他的路径,这里我们直接用Google提供的@AutoService(Processor.class)注解的方式来配置我们的注解类路径。下面先贴下我们的注解处理器都做了哪些工作
@AutoService(Processor.class)
public class MyknifeProcessor extends AbstractProcessor{
private Filer mFiler;
private Elements mElements;
private Messager mMessager;
private Map annotateBindingMap = new LinkedHashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElements = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}/**
*
* @param annotations 请求处理的注解类型
* @param roundEnv 表示当前或是之前的运行环境
* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
*如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
*
*简单点说就是当一个方法被2个注解同时修饰后,若第一个注解处理器的process方法返回了true,那么第二个注解处理器就不会处理该方法了
*/
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
//避免重复
annotateBindingMap.clear();
processBindView(roundEnv);
processOnClick(roundEnv);
try {
//遍历map中的value值,即:AnnotateBindingSet
for (AnnotateBindingSet annotateBindingSet : annotateBindingMap.values()) {
annotateBindingSet.dealClassWrite().writeTo(mFiler);
}
} catch (IOException e) {
e.printStackTrace();
error(String.format("Unable to write binding for type: %s", e.getMessage()));
}return false;
}private void processBindView(RoundEnvironment roundEnv) {
//此方法获取的是指定注解修饰的元素集合,返回的是element及其子类对象
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
//getEnclosingElement()获取父节点, getEnclosedElements()获取所有的子节点
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String fullName = typeElement.getQualifiedName().toString();
AnnotateBindingSet annotateBindingSet = annotateBindingMap.get(fullName);
if (annotateBindingSet == null) {
annotateBindingSet = new AnnotateBindingSet(typeElement, mElements);
annotateBindingMap.put(fullName, annotateBindingSet);
}
BindViewField field = new BindViewField(element);
//将field添加到AnnotateBindingSet中
annotateBindingSet.addFields(field);
}
}private void processOnClick(RoundEnvironment roundEnv) {
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(OnClick.class);
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String fullName = typeElement.getQualifiedName().toString();
AnnotateBindingSet annotateBindingSet = annotateBindingMap.get(fullName);
if (annotateBindingSet == null) {
annotateBindingSet = new AnnotateBindingSet(typeElement, mElements);
annotateBindingMap.put(fullName, annotateBindingSet);
}
OnClickMethod method = new OnClickMethod(element);
annotateBindingSet.addMethod(method);
}
}@Override
public Set getSupportedAnnotationTypes() {
Set annotationTypes = new LinkedHashSet<>();
annotationTypes.add(BindView.class.getCanonicalName());
annotationTypes.add(OnClick.class.getCanonicalName());
return annotationTypes;
}@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}private void error(String msg){
mMessager.printMessage(Diagnostic.Kind.ERROR, msg);
}
}
这里就简单介绍下吧,①init方法:会被注解处理工具调用并传进来ProcessingEnvironment,这个ProcessingEnvironment里面提供了很多非常有用的工具类,例如代码中的filer是个接口支持通过注解处理器创建文件,Messager可以用来反馈一些信息,比如注解处理器处理失败等,这些信息都可以从这个Messager里面拿到。②getSupportedAnnotationTypes()方法:主要是为了指明你这个注解处理器是注册到哪些注解上,返回值是一个字符串的集合,包含此注解处理器想要处理的注解的合法全称。③getSupportedSourceVersion()方法:用来指定你使用的java版本信息。④process方法:这是注解处理器的核心main方法,主要用来解析注解并生成需要的.java文件,其实最麻烦的就是动态生成.java类文件代码,这个时候我们可以使用方块公司提供的javapoet来实现生成我们需要的.java文件。
我开始在做的时候所有的逻辑处理都直接写在了process方法中,结果是又臭又长可读性也不高,这里要感谢这位前辈https://blog.csdn.net/ta893115871/article/details/52497495的面向对象编程,封装成对象,改成前辈的这种思路之后顿感清爽啊。这里我大致讲下几个api:
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);此方法是拿到被BindView注解修饰的元素集合,比如文章开头代码中的TextView tvAnnotate、Button btn 等。
TypeElement typeElement = (TypeElement) element.getEnclosingElement();getEnclosingElement()表示获取此元素对应的父节点(例如上文提到的tvAnnotate、btn等元素的父节点就是MainActivity;getEnclosedElements()表示获取所有的子节点
BindViewField field = new BindViewField(element);【手拉手系列之造一个简易的ButterKnife学习编译时注解】如果被BindView注解修饰的元素是成员变量的话(例如TextView tvAnnotate),那么就强转成成员变量类型,然后通过getAnnotation()方法获取指定类型的注解,拿到注解之后就可以拿到注解中定义的值了,在这里就是我们赋的控件id值,最后再把这些包含我们需要信息的对象再封装到AnnotateBindingSet对象中,而AnnotateBindingSet主要就是利用javapoet生成.java源文件,接下里我们就看下AnnotateBindingSet这个类都做了哪些工作?
VariableElement variableElement = (VariableElement) element;
BindView bindView = variableElement.getAnnotation(BindView.class);
/**
* 主要处理文件的写入逻辑
*/
public final class AnnotateBindingSet {
private TypeElement typeElement;
//表示类元素
private Elements elementUtils;
//工具
private List fieldLists;
private List methodLists;
public AnnotateBindingSet(TypeElement typeElement, Elements elements){
this.typeElement = typeElement;
this.elementUtils = elements;
fieldLists = new ArrayList<>();
methodLists = new ArrayList<>();
}public void addFields(BindViewField bindViewField){
fieldLists.add(bindViewField);
}public void addMethod(OnClickMethod onClickMethod){
methodLists.add(onClickMethod);
}public JavaFile dealClassWrite(){
String packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//完整的包名
String className = getClassName(typeElement, packageName);
ClassName bindingClassName = ClassName.get(packageName, className);
System.out.println("className:"+className);
System.out.println("typeElement--getSimpleName:"+typeElement.getSimpleName().toString());
System.out.println("bindingClassName--simpleName:"+bindingClassName.simpleName());
//类名
TypeSpec.Builder typeSpec = TypeSpec.classBuilder(bindingClassName.simpleName()+"$$Injector")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(typeElement.asType())));
//解绑方法
MethodSpec.Builder unBindMethod = MethodSpec.methodBuilder("unBind")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeName.get(typeElement.asType()), "host");
//绑定方法
MethodSpec.Builder main = MethodSpec.methodBuilder("inject")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.PROVIDER, "provider");
//处理方法中field的代码
for (BindViewField field : fieldLists) {
main.addStatement("host.$L = ($T) provider.findViewById(source, $L)",
field.getFieldName(),
ClassName.get(field.getFieldType()),
field.getIdOfView());
unBindMethod.addStatement("host.$L = null", field.getFieldName());
}
//处理onclick
main.addStatement("$T listener", TypeUtil.ONCLICKLISTENER);
for (OnClickMethod method : methodLists) {
//匿名内部类
TypeSpec clickTypeSpec = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(TypeUtil.ONCLICKLISTENER)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeUtil.VIEW, "view")
.addStatement("host.$N($L)", method.getMethodName(), method.getMethodParamName())
.build())
.build();
main.addStatement("listener = $L", clickTypeSpec);
for (int id : method.getIds()) {
String fieldName = "view" + id;
//Integer.toHexString(id)
typeSpec.addField(TypeUtil.VIEW, fieldName, Modifier.PRIVATE);
main.addStatement("$L = provider.findViewById(source, $L)", fieldName, id);
main.addStatement("$L.setOnClickListener(listener)", fieldName);
unBindMethod.addStatement("$L.setOnClickListener(null)", fieldName);
unBindMethod.addStatement("$L = null", fieldName);
//main.addStatement("provider.findViewById(source, $L).setOnClickListener(listener)", id);
}
}typeSpec.addMethod(main.build()).addMethod(unBindMethod.build());
return JavaFile.builder(packageName, typeSpec.build()).build();
}private String getClassName(TypeElement typeElement, String packageName) {
System.out.println("包名:"+packageName);
//+1是为了最终substring截取的是从类名开始 eg:com.test.CommonActivity(完整的类名格式=包名+"."+类名),保证最终截取的值是CommonActivity
int packageLength = packageName.length() + 1;
String clazzName = typeElement.getQualifiedName().toString();
//获取完整的类名(包含包名)
System.out.println("完整格式的类名:"+clazzName);
System.out.println("截取之后的类名:"+clazzName.substring(packageLength));
return clazzName.substring(packageLength).replace('.', '$');
}
}
额外说明下,这里面用到的美元符号后面的字母不能乱写的(例如N),用之前还是需要到官网去看,根据官网给出的使用规则来。我之前写的时候就比较随意,随便给个字母结果始终没有出来期望的效果。最终就是生成了文章开头给出的那个.java类文件,这块内容是最麻烦的,这里有个窍门,因为我们是要模仿ButterKnife,顺便搞清楚ButterKnife的主要实现原理,所以我们可以先写个demo然后集成ButterKnife编译之后会生成我们需要的.java类文件,我们可以参考他的格式然后在AnnotateBindingSet中去构造出来这个类以及里面的方法字段等。
annotationProcessor 我们在引入ButterKnife这个库的时候是这样的
dependencies {可能你会好奇这个annotationProcessor是干嘛的?之前我们会使用apt,只不过现在好像apt不再维护了,转而使用Google提供的annotationProcessor。这里简单说下annotationProcessor是注解处理器的辅助工具,主要作用如下:①允许编译时注解处理器依赖,但在最终打包apk时,自定义的这个注解处理器的代码是不会打包到apk中的,降低了代码的出错率;②这个工具可以自动的帮你为生成的.java源文件创建目录,并且能让android studio正确的引用到这个目录,让这个生成的.java源文件代码编译到apk中,提供程序使用。
implementation 'com.jakewharton:butterknife:10.0.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0'
}
其实demo很早之前就抽时间写好了,年底了公司任务比较多一直也没时间去写博客,现在产品上线了利用这段时间赶紧记录下来。其实注解真的挺重要的,当你去绞尽脑汁的阅读一些第三方框架源码的时候你就会发现里面几乎都离不开注解。好了,扯了这么多目的就是希望自己能温习下注解以及能对读者有些许的帮助吧~
demo随后会上传到GitHub
感谢前辈的文章
深入理解Java注解类型(@Annotation)
深入理解ButterKnife源码并掌握原理
JakeWharton/butterknife
square/javapoet
推荐阅读
- 【欢喜是你·三宅系列①】⑶
- 你不可不知的真相系列之科学
- 人脸识别|【人脸识别系列】| 实现自动化妆
- 2018-06-13金句系列7(金句结构-改编古现代诗词)
- Unity和Android通信系列文章2——扩展UnityPlayerActivity
- 乡野村趣系列之烧仙草
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)
- 15、IDEA学习系列之其他设置(生成javadoc、缓存和索引的清理等)
- 【年终激励系列】之五(年终奖如何与考核紧密相连)
- 剥削劳动力系列(企业家剥削你时,他要付出巨大的代价)