从这次实战我能学会什么
实战要实现的功能
完全仿写ButterKnife的bindView注解原理,实现在Activity中控件id赋值功能
实战看点
这次实战是从源码出发,仿照ButterKnife的源码实现其中的BindView的功能,实际上就是把ButterKnife的BindView的逻辑剥离出来单独实现,代码量少了许多但与源码差别不大,达到了练习注解的目的。所以我们学会了这个,你会发现,看ButterKnife的源码突然变得简单了
实战基础
了解并能运用AutoService,JavaPoet,AbstractProcessor
不熟悉的,请先参考我之前的文章:安卓使用注解处理器自动生成代码操作详解(AutoService,JavaPoet,AbstractProcessor)
下面我们就来通过实战探究ButterKnife的注解奥秘
ButterKnife原理的简单介绍
既然要仿写,必然要先对仿写对象的实现原理要有一些了解
这是它的简单使用:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv1)
public TextView textView1;
private Unbinder unbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
unbinder = ButterKnife.bind(this);
//试着运行一下吧
textView1.setText("这是一个赋值测试");
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
}
运行后我们会发现生成了一个MainActivity_ViewBinding
文件,结合我上篇文章我们知道这是使用了注解处理器:
// Generated code from Butter Knife. Do not modify!
package com.jay.bindview;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import butterknife.Unbinder;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.textView1 = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'textView1'", TextView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.textView1 = null;
}
}
欸,是不是执行了MainActivity_ViewBinding
的构造方法就完成了控件赋值了,那么这个方法在哪执行的呢,看ButterKnife.bind(this)
方法,最终会到这:
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
注意constructor.newInstance(target, source);
这行代码,这是通过反射获取到类然后执行这个类的构造方法,这样一分析,实现流程是不是一目了然了,先通过BindView注解获取到控件的id,控件的类型和控件父类的名称(用于生成特定的类文件名和控件赋值),然后生成代码,最后通过反射执行生成的类的构造方法,是不是就实现了我们想要的功能。
下面开始撸码
BindView代码实现
既然是仿,我们仿的像一点,先新建一个项目,创建3个library:
- bindview-annotation 注意是java library,用于存放注解
- bindview-compiler 注意还是java library,用于处理注解,生成代码
- bindviewlib 安卓library,用于反射调用生成的代码
bindviewlib 和bindview-compiler都需要依赖bindview-annotation
项目结构如图所示:
我们先在bindview-annotation中创建一个BindView注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value();
}
随后,我们就按照源码在bindview-compiler
中新建4个类:
- BindingSet 代码统筹管理类,处理代码生成逻辑
- FieldViewBinding 用来保存字段的信息
- ID 用来保存id信息
- ViewBinding 用来保存FieldViewBinding 和 ID的实例,方便管理和缓存
先看FieldViewBinding
,就简单的保存了两个字段信息,TypeName
是字段的类型,相当于我的生成的代码里的TextView
,而name
相当于我们的代码里的textview1
final class FieldViewBinding {
//字段名称
private final String name;
//字段类型
private final TypeName type;
FieldViewBinding(String name,TypeName type){
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public TypeName getType() {
return type;
}
public ClassName getRawType() {
if (type instanceof ParameterizedTypeName) {
return ((ParameterizedTypeName) type).rawType;
}
return (ClassName) type;
}
}
再看ID
类,value
就是我们从注解中获取到的id,传入到CodeBlock
中方便生成代码
final class ID {
/**
* value及注解中的value id
*/
final CodeBlock code;
ID(int value){
this.code = CodeBlock.of("$L", value);
}
}
再来看一下ViewBinding
类,仿照源码用了构建者模式,保存了ID
和FieldViewBinding
的值:
final class ViewBinding {
private final ID id;
@Nullable
private final FieldViewBinding fieldBinding;
private ViewBinding(ID id, @Nullable FieldViewBinding fieldBinding) {
this.id = id;
this.fieldBinding = fieldBinding;
}
public ID getId() {
return id;
}
@Nullable
public FieldViewBinding getFieldBinding() {
return fieldBinding;
}
static final class Builder {
private final ID id;
@Nullable
private FieldViewBinding fieldBinding;
Builder(ID id) {
this.id = id;
}
public void setFieldBinding(FieldViewBinding fieldBinding) {
if (this.fieldBinding != null) {
throw new AssertionError();
}
this.fieldBinding = fieldBinding;
}
public ViewBinding build() {
return new ViewBinding(id, fieldBinding);
}
}
}
最后就是我们的核心类BindingSet
了,看看是怎么来创建代码的吧:
final class BindingSet{
private final TypeName targetTypeName; //示例值 MainActivity
private final ClassName bindingClassName; //示例值 MainActivity_ViewBinding
private final TypeElement enclosingElement; //这是注解元素的父类Element,用于获取父类元素
private final ImmutableList viewBindings; //保存了每一个字段的元素
private BindingSet(
TypeName targetTypeName, ClassName bindingClassName, TypeElement enclosingElement,
ImmutableList viewBindings) {
this.targetTypeName = targetTypeName;
this.bindingClassName = bindingClassName;
this.enclosingElement = enclosingElement;
this.viewBindings = viewBindings;
}
/**
* 从这个方法开始构建代码,这里只实现BindView的代码逻辑
*
* @return JavaFile
*/
JavaFile brewJava() {
TypeSpec bindingConfiguration = createType();
return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
private TypeSpec createType() {
//第一步 先创建类
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(Modifier.PUBLIC)
.addOriginatingElement(enclosingElement); //设置注解处理器的源元素
//添加解绑接口
result.addSuperinterface(ClassName.get("com.jay.bindviewlib", "Unbinder"));
//添加activity字段target
result.addField(targetTypeName, "target");
//添加构造方法
result.addMethod(createBindingConstructorForActivity());
//添加找id的方法
result.addMethod(createBindingConstructor());
//添加解绑的方法
result.addMethod(createBindingUnbindMethod());
return result.build();
}
/**
* 示例:MainActivity_BindView(MainActivity target){
* this(target, target.getWindow().getDecorView())
* }
*
* @return MethodSpec
*/
private MethodSpec createBindingConstructorForActivity() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(PUBLIC)
.addParameter(targetTypeName, "target");
builder.addStatement("this(target, target.getWindow().getDecorView())");
return builder.build();
}
private static final ClassName VIEW = ClassName.get("android.view", "View");
/**
* 创建构造方法,这个方法里包含找id的代码
*
* @return MethodSpec
*/
private MethodSpec createBindingConstructor() {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PUBLIC);
constructor.addParameter(targetTypeName, "target");
constructor.addParameter(VIEW, "source");
constructor.addStatement("this.target = target");
constructor.addCode("\n");
//这里循环创建控件赋值代码
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding);
}
return constructor.build();
}
//创建一条赋值代码
//示例:target.textview1 = (TextView)source.findViewById(id)
//这里的source = target.getWindow().getDecorView() target是Activity
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
FieldViewBinding fieldBinding = requireNonNull(binding.getFieldBinding());
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName()); //添加代码 target.textview1 =
builder.add("($T) ", fieldBinding.getType()); //添加强转代码
builder.add("source.findViewById($L)", binding.getId().code); //找id
result.addStatement("$L", builder.build()); //将代码添加到方法中
}
/**
* 创建解绑的方法
*
* @return MethodSpec
*/
private MethodSpec createBindingUnbindMethod() {
MethodSpec.Builder result = MethodSpec.methodBuilder("unbind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC);
result.addStatement("$T target = this.target", targetTypeName);
result.addStatement("if (target == null) throw new $T($S)", IllegalStateException.class,
"Bindings already cleared.");
result.addStatement("$N = null","this.target");
result.addCode("\n");
for (ViewBinding binding : viewBindings) {
if (binding.getFieldBinding() != null) {
result.addStatement("target.$L = null", binding.getFieldBinding().getName());
}
}
return result.build();
}
/**
* 生成代码生成的类的类名
* @return Name 规则 ActivityName__ViewBinding
*/
static ClassName getBindingClassName(TypeElement typeElement) {
String packageName = getPackage(typeElement).getQualifiedName().toString();
String className = typeElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
return ClassName.get(packageName, className + "_ViewBinding");
}
/**
* 创建一个Builder
* @param enclosingElement 父类元素,也就是那个Activity
* @return 这里生成了类名称与类target
*/
static Builder newBuilder(TypeElement enclosingElement) {
TypeMirror typeMirror = enclosingElement.asType();
TypeName targetType = TypeName.get(typeMirror);
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
ClassName bindingClassName = getBindingClassName(enclosingElement);
return new Builder(targetType, bindingClassName, enclosingElement);
}
static final class Builder {
private final TypeName targetTypeName;
private final ClassName bindingClassName;
private final TypeElement enclosingElement;
//缓存ViewBinding实例,提升性能
private final Map viewIdMap = new LinkedHashMap<>();
private Builder(
TypeName targetTypeName, ClassName bindingClassName, TypeElement enclosingElement) {
this.targetTypeName = targetTypeName;
this.bindingClassName = bindingClassName;
this.enclosingElement = enclosingElement;
}
void addField(ID id, FieldViewBinding binding) {
getOrCreateViewBindings(id).setFieldBinding(binding);
}
private ViewBinding.Builder getOrCreateViewBindings(ID id) {
ViewBinding.Builder viewId = viewIdMap.get(id);
if (viewId == null) {
viewId = new ViewBinding.Builder(id);
viewIdMap.put(id, viewId);
}
return viewId;
}
BindingSet build() {
ImmutableList.Builder viewBindings = ImmutableList.builder();
for (ViewBinding.Builder builder : viewIdMap.values()) {
viewBindings.add(builder.build());
}
return new BindingSet(targetTypeName, bindingClassName, enclosingElement, viewBindings.build());
}
}
}
这个类完全仿照源码编写,只保留了Activity
的赋值逻辑,先来看用到的四个参数的作用:
-这个是控件的父类的类型名称,用于生成target的值
private final TypeName targetTypeName;
-这个是生成的文件名称
private final ClassName bindingClassName;
-这个是注解的父注解元素
private final TypeElement enclosingElement;
-这个就是我们的字段信息的缓存集合了
private final ImmutableList viewBindings;
我们得先获取到这4个参数的值,这是一个构建者模式,构建者的赋值逻辑在newBuilder
方法中:
/**
* 创建一个Builder
* @param enclosingElement 父类元素,也就是那个Activity
* @return 这里生成了类名称与类target
*/
static Builder newBuilder(TypeElement enclosingElement) {
TypeMirror typeMirror = enclosingElement.asType();
TypeName targetType = TypeName.get(typeMirror);
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
ClassName bindingClassName = getBindingClassName(enclosingElement);
return new Builder(targetType, bindingClassName, enclosingElement);
}
看一下getBindingClassName
方法是如何获取到名称的:
/**
* 生成代码生成的类的类名
* @return Name 规则 ActivityName__ViewBinding
*/
static ClassName getBindingClassName(TypeElement typeElement) {
String packageName = getPackage(typeElement).getQualifiedName().toString();
String className = typeElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
return ClassName.get(packageName, className + "_ViewBinding");
}
可以看到是通过父元素的getQualifiedName
方法获取到标准格式的类名后截取的,随后手动添加了_ViewBinding后缀。
我们还有一个viewBindings
没有赋值,这个值需要从注解器里去拿到,这个随后再说,下面我们看代码生成逻辑,入口是brewJava
方法:
JavaFile brewJava() {
TypeSpec bindingConfiguration = createType();
return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
可以看到通过createType
获取到TypeSpec
就直接生成JavaFile
了,看一下createType
方法做了什么:
private TypeSpec createType() {
//第一步 先创建类
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(Modifier.PUBLIC)
.addOriginatingElement(enclosingElement); //设置注解处理器的源元素
//添加解绑接口
result.addSuperinterface(ClassName.get("com.jay.bindviewlib", "Unbinder"));
//添加activity字段target
result.addField(targetTypeName, "target");
//添加构造方法
result.addMethod(createBindingConstructorForActivity());
//添加找id的方法
result.addMethod(createBindingConstructor());
//添加解绑的方法
result.addMethod(createBindingUnbindMethod());
return result.build();
}
通过TypeSpec.Builder
依次添加各个部分的代码,逻辑还是比较清晰的,需要注意的是,添加Unbinder
类传入包名的时候要填写正确的路径哦,不要直接把我的包名复制进去了。看不太懂的结合生成的代码比较着看,相信很容易就能看懂,这里与源码是有差别的,源码中是使用的Utils
来寻找id,我这里为了方便直接生成了findviewbyid
的代码,注意区别!!
代码生成的逻辑写好了,接下来就到了我们的注解处理器上了,我们的BindingSet
需要从处理器中获取到字段信息和控件的父元素信息才能创建代码对吧,接下来请看:
我们新建一个类名叫BindViewProcessor
:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//我们可以从这里获取一些工具类
mFiler = processingEnvironment.getFiler();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//缓存BindingSet并给BindingSet赋值
Map bindingMap = findAndParseTargets(roundEnvironment);
//第二步,循环获取BindingSet并执行brewJava开始绘制代码
for (Map.Entry entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 给BindingSet赋值并生成一个map
* @param env 当前元素环境
* @return 元素集合
*/
private Map findAndParseTargets(RoundEnvironment env) {
Map builderMap = new LinkedHashMap<>();
Set erasedTargetNames = new LinkedHashSet<>();
//这里循环生成了BindingSet.Builder并将值放入了builderMap中
Set extends Element> envs = env.getElementsAnnotatedWith(BindView.class);
for (Element element : envs) {
try {
parseBindView(element, builderMap);
} catch (Exception e) {
e.printStackTrace();
}
}
//从builderMap中取出值并生成BindingSet放入bindingMap中,源码是用的while,并有处理父类的super逻辑,这里直接用for
Map bindingMap = new LinkedHashMap<>();
for (Map.Entry entry:builderMap.entrySet()) {
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
bindingMap.put(type, builder.build());
}
return bindingMap;
}
/**
* 为BindingSet赋值,从Element元素中获取Activity与控件信息,并保存到BindingSet中
*/
private void parseBindView(Element element, Map builderMap) {
//获取父类的Element
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
builder.addField(new ID(id), new FieldViewBinding(name, type));
}
/**
* 创建BindingSet 并且将BindingSet缓存到builderMap中
*/
private BindingSet.Builder getOrCreateBindingBuilder(
Map builderMap, TypeElement enclosingElement) {
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName()); //将我们自定义的注解添加进去
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
其他3个方法基本都是样板代码,着重看process
方法,首先是通过findAndParseTargets
方法获取到BindingSet
的缓存,BindingSe
t的赋值逻辑在parseBindView
方法中:
/**
* 为BindingSet赋值,从Element元素中获取Activity与控件信息,并保存到BindingSet中
*/
private void parseBindView(Element element, Map builderMap) {
//获取父类的Element
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
builder.addField(new ID(id), new FieldViewBinding(name, type));
}
我们通过element.getEnclosingElement();
方法就能获取到控件的父元素,这是BindingSet
需要的值得其中之一,随后通过element.getAnnotation(BindView.class).value();
获取到id
并将它保存到ID
类中,随后通过getSimpleName
获取到控件的名称,也就是我们生成的代码的那个textview1
名称,控件类型,也就是我们的那个TextView
,可以通过element.asType()
先获取到控件的信息类TypeMirror
,随后通过TypeName.get
方法获取到TypeName
的实例,知道了TypeName
,我们就相当于在代码中持有了这个类的实例,我们就能直接把它作为参数传入到JavaPoet方法构建中去了,最后我们通过builder.addField(new ID(id), new FieldViewBinding(name, type));
方法传入ID
和FieldViewBinding
类,保存到了viewBindings
中,需要的值也就都赋值完毕了。
既然值都获取到了,我们回到process
方法,我们已经获取到了所有用BindView
标记的控件的一个集合,接下来当然是循环调用brewJava
方法构建代码啦:
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//缓存BindingSet并给BindingSet赋值
Map bindingMap = findAndParseTargets(roundEnvironment);
//第二步,循环获取BindingSet并执行brewJava开始绘制代码
for (Map.Entry entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
核心代码已经写好了,接下来就剩下调用方法啦,我们在bindviewlib
中新建一个类BindViewHelper
,我们可以直接将ButterKnife
类的代码搬过来,偷一波懒,最后,我们在申明一个解绑的接口:
public interface Unbinder {
@UiThread
void unbind();
Unbinder EMPTY = () -> { };
}
整个代码就写完了,接下来就在Activity中实验一下吧,我们在app的build.gradle中引入这两个包:
implementation project(':bindviewlib')
annotationProcessor project(':bindview-compiler')
在代码中使用:
/**
* 自动生成的代码位于build>generated>source>apt目录下
*/
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv1)
public TextView textView1;
@BindView(R.id.tv2)
public TextView textView2;
private Unbinder unbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
unbinder = BindViewHelper.bind(this);
//试着运行一下吧
textView1.setText("这是一个赋值测试");
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
}
运行一下,我们在build>generated>source>apt
目录下发现成功生成了文件MainActivity_ViewBinding
// Generated code from Butter Knife. Do not modify!
package com.jay.bindview;
import android.view.View;
import android.widget.TextView;
import com.jay.bindviewlib.Unbinder;
import java.lang.IllegalStateException;
import java.lang.Override;
public class MainActivity_ViewBinding implements Unbinder {
MainActivity target;
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.textView1 = (TextView) source.findViewById(2131165312);
target.textView2 = (TextView) source.findViewById(2131165313);
}
@Override
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.textView1 = null;
target.textView2 = null;
}
}
大功告成!!
结尾
先给个demo地址,方便读者查阅代码:https://github.com/Jay-huangjie/BindView
希望读者能参照我的代码重新写一遍,代码并不复杂,相信对注解会有新的理解,然后你再去看ButterKnife的源码,会发现出奇的熟悉,一下就看懂了,有任何的问题欢迎留言,关注我,不迷路