butterknife源码分析系列:
谈一谈Java的注解
http://www.jb51.cc/article/p-srjxthnw-bpg.html
如何处理注解—反射与注解处理器
http://www.jb51.cc/article/p-ppftpdqn-bpg.html
代码分析
http://www.jb51.cc/article/p-rnvspoko-bpg.html
前面两篇讲解了注解的定义,以及如何用反射与注解处理器的方法来处理注解。在第二篇文章中,我们用反射的方式模拟了butterknife的findViewById来简化代码,同时,我们也说过butterknife其实是用注解处理器来实现的。
虽然butterknife的源码解析文章已有许多,这里借其肩膀,总结总结。
流程介绍
上文我们详细介绍了注解处理器,这里再结合butterknife再次强调下。在编译源文件时,会分析扫描注解,当扫描到butterknife定义的@BindView、@OnClick等注解时,会使用JavaPoet来生成代码。生成后的文件会再次分析,直到没有分析到需要处理的注解位置。
JavaPoet简介
Poet译为诗人,JavaPoet可以帮助便捷地生成代码,而不是手动繁琐的拼接语句。简要介绍下比较关键的几个类:
详细的使用方法可以看github上的介绍:
从结果入手
直接从源码分析容易一头雾水。既然ButterKnife会在编译时生成代码,那我们从结果入手,看看生成的代码长什么样。
源文件:
public class MainActivity extends Activity {
@BindView(R.id.tv_title)
public TextView tvTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.tv_title)
void titleClick() {
}
}
编译生成的文件可以在build/source/apt下可以看到:
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131034112;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target,target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target,View source) {
this.target = target;
View view;
view = Utils.findrequiredView(source,R.id.tv_title,"field 'tvTitle' and method 'titleClick'");
target.tvTitle = Utils.castView(view,"field 'tvTitle'",TextView.class);
view2131034112 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.titleClick();
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.tvTitle = null;
view2131034112.setOnClickListener(null);
view2131034112 = null;
}
}
注意到源文件名是MainActivity,而生成的文件是MainActivity_ViewBinding。
在构造函数内,使用
target.tvTitle = Utils.findrequiredViewAsType(source,R.id.tv_title,TextView.class);
利用Utils.findrequiredViewAsType得到的结果赋值到我们定义的TextView(tvTitle)上,这也解释了为什么tvTitle需要用public来修饰。
进一步看看findrequiredViewAsType。
public static <T> T findrequiredViewAsType(View source,@IdRes int id,String who,Class<T> cls) {
View view = findrequiredView(source,id,who);
return castView(view,who,cls);
}
castView方法是将得到的View转化成具体的子View,这里是TextView。而findrequiredView里进行了findViewById的操作。
这里我们可能会有疑问,在生成的MainActivity_ViewBinding的构造方法使用到MainActivity,而我们在使用ButterKnife时会使用ButterKnife.bind(this)将Activity传递到ButterKnife里,这之间是怎么一个过程?
进去ButterKnife.bind(this)看看。
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target,sourceView);
}
根据activity得到DecorView,再传递到createBinding。
private static Unbinder createBinding(@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后,再运用反射生成实例,在实例里的findView操作就会被调用到。接下来看看如何获取到Constructor的。
@Nullable @CheckResult @UiThread private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) { Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); if (bindingCtor != null) { if (debug) Log.d(TAG,"HIT: Cached in binding map."); return bindingCtor; } String clsName = cls.getName(); // 过滤掉系统相关的类 if (clsName.startsWith("android.") || clsName.startsWith("java.")) { if (debug) Log.d(TAG,"MISS: Reached framework class. Abandoning search."); return null; } try { Class<?> bindingClass = Class.forName(clsName + "_ViewBinding"); //noinspection unchecked bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls,View.class); if (debug) Log.d(TAG,"HIT: Loaded binding class and constructor."); } catch (ClassNotFoundException e) { if (debug) Log.d(TAG,"Not found. Trying superclass " + cls.getSuperclass().getName()); bindingCtor = findBindingConstructorForClass(cls.getSuperclass()); } catch (NoSuchMethodException e) { throw new RuntimeException("Unable to find binding constructor for " + clsName,e); } BINDINGS.put(cls,bindingCtor); return bindingCtor; }
从上面可以看到Constructor获取的过程,根据className得到className_ViewBinding,就可以得到Constructor。并且会将得到的Constructor缓存起来,避免反射的性能问题。
这样一来,ButterKnife.bind(this)传递进去的MainActivity会通过反射生成MainActivity_ViewBinding实例。在这个实例的构造函数内,进行findViewById、setOnclickListener等操作
编译过程
MainActivity在编译时会生成处理findViewById等操作的MainActivity_ViewBinding。接下来我们探索下ButterKnife偷偷在注解处理器里做了什么。
关于注解处理器:
注解处理器里包含下面几个重要的方法:
从process方法入手:
@Override public boolean process(Set<? extends TypeElement> elements,RoundEnvironment env) {
Map<TypeElement,BindingSet> bindingMap = findAndParseTargets(env);
// 利用JavaPoet生成代码
for (Map.Entry<TypeElement,BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement,"Unable to write binding for type %s: %s",typeElement,e.getMessage());
}
}
return false;
}
process里主要做了两件事情:
findAndParseTargets
跟踪进入findAndParseTargets方法。
private Map<TypeElement,BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement,BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
// 建立view与R的id的关系
scanForRClasses(env);
// 省略部分代码
// 解析BindView注解
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element,builderMap,erasedTargetNames);
} catch (Exception e) {
logParsingError(element,BindView.class,e);
}
}
// 省略部分代码
// 将Map.Entry<TypeElement,BindingSet.Builder>转化为Map<TypeElement,BindingSet>
Deque<Map.Entry<TypeElement,BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement,BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry<TypeElement,BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type,erasedTargetNames);
if (parentType == null) {
bindingMap.put(type,builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type,builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
}
以BindView为例,省略掉无关的代码,findAndParseTargets分成三部分,第一部分是scanForRClasses,第二部分是解析各种注解,最后部分是根据buildMap来生成bindingMap。
① scanForRClasses
scanForRClasses主要是用来建立view与id的关系。
例如在上面所举的栗子中,MainActivity_ViewBinding里会持有一个全局变量view2131034112,这个其实就是MainActivity的tvTitle,后面的2131034112就是对应在R文件的id。
值得一提的是,这部分代码在旧版本的butterknife并没有出现,估计是为了解决某些潜在的bug。
https://github.com/JakeWharton/butterknife/issues/770
It reads the source code to map the IDs the processor sees back to the resource names for the generated code.
这里简要介绍下,scanForRClasses涉及到Java的语法分析树。
R文件示例:
public final class R {
public static final class attr {
}
public static final class id {
public static final int tv_title=0x7f050000;
}
public static final class layout {
public static final int activity_main=0x7f030000;
}
public static final class mipmap {
public static final int ic_launcher=0x7f020000;
}
public static final class string {
public static final int app_name=0x7f040000;
}
}
首先根据element获取到包名,再利用RClassScanner寻找到R文件,在R文件里利用IdScanner寻找到内部类id,在id类里利用VarScanner寻找到tvTitle的id。最后就可以得到view2131034112。
② parseBindView
第二部分是解析各种注解,这里以BindView为例。
private void parseBindView(Element element,Map<TypeElement,BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) {
// 得到包含注解所属的TypeElement,例如MainActivity
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// isInaccessibleViaGeneratedCode检验enclosingElement(MainActivity)是类、不是private,检验element不是private活着static
// isBindingInWrongPackage检验enclosingElement的包名是不是系统相关的类
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class,"fields",element) || isBindingInWrongPackage(BindView.class,element);
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
// 判断element是View的子类或者接口
if (!isSubtypeOfType(elementType,VIEW_TYPE) && !isInterface(elementType)) {
if (elementType.getKind() == TypeKind.ERROR) {
note(element,"@%s field with unresolved type (%s) "
+ "must elsewhere be generated as a View or interface. (%s.%s)",BindView.class.getSimpleName(),elementType,qualifiedName,simpleName);
} else {
error(element,"@%s fields must extend from View or be an interface. (%s.%s)",simpleName);
hasError = true;
}
}
if (hasError) {
return;
}
// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);
QualifiedId qualifiedId = elementToQualifiedId(element,id);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
// 检查是否绑定过此id
if (existingBindingName != null) {
error(element,"Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",existingBindingName,enclosingElement.getQualifiedName(),element.getSimpleName());
return;
}
} else {
builder = getOrCreateBindingBuilder(builderMap,enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldrequired(element);
builder.addField(getId(qualifiedId),new FieldViewBinding(name,type,required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
parseBindView先检测是否有错误,然后将name(变量名,例如tvTitle)、type(类名,例如TextView)、required(是否有@nullable注解)封装成FieldViewBinding放到builder里面。
③ Map.Entry<TypeElement,BindingSet.Builder> -> Map<TypeElement,BindingSet>
Deque<Map.Entry<TypeElement,builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
遍历builderMap,先通过findParentType判断是否有父类,若有,通过setParent来设置进去。在生成代码时,若有父类,会把父类的注解也注入。findParentType方法是通过TypeMirror来获取父类的信息(TypeMirror可以获取类里面的方法、域、超类等信息)。
运用JavaPoet框架来生成代码
在生成Map<TypeElement,BindingSet>
后,下一步就是使用JavaPoet框架生成相关的代码。
@Override public boolean process(Set<? extends TypeElement> elements,RoundEnvironment env) {
Map<TypeElement,BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement,e.getMessage());
}
}
return false;
}
遍历Map<TypeElement,BindingSet>,根据TypeElement(MainActivity)利用Filer工具类来生成文件TypeElement_ViewBinding(MainActivity_ViewBinding)。
看看brewJava方法:
JavaFile brewJava(int sdk) {
return JavaFile.builder(bindingClassName.packageName(),createType(sdk))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
brewJava比较简单,逻辑都在createType(sdk)这里面。
private TypeSpec createType(int sdk) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}
// 如果有父类,继承自父类。
if (parentBinding != null) {
result.superclass(parentBinding.bindingClassName);
} else { // 否则,实现UNBINDER接口。
result.addSuperinterface(UNBINDER);
}
// 增加target全局变量。例如上面的栗子,MainActivity target。
if (hasTargetField()) {
result.addField(targetTypeName,"target",PRIVATE);
}
// 入参为target的构造函数①,主要是调用下面的构造函数②
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
result.addMethod(createBindingViewDelegateConstructor());
}
// 构造函数②
result.addMethod(createBindingConstructor(sdk));
// 生成unbind方法
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
createType方法返回的是TypeSpec,TypeSpec是JavaPoet框架中用来生成类的。这个类里面最重要的代码是在构造函数。跟着进去生成构造函数的方法createBindingUnbindMethod。
private MethodSpec createBindingConstructor(int sdk) {
// public修饰、@UiThread注解
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
// 如果注解是修饰方法的,则入参的target为final
// 当注解是修饰方法时,都是些监听注解,例如OnClick、OnTouch等。这些生成代码时,会用到匿名内部类,所以需要target为final的。
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName,FINAL);
} else {
constructor.addParameter(targetTypeName,"target");
}
if (constructorNeedsView()) {
constructor.addParameter(VIEW,"source");
} else {
constructor.addParameter(CONTEXT,"context");
}
// 当需要传入R.xx.xx时,传入数字会提示expected resource of type xx。因此增加@SuppressWarnings(“ResourceType”)注解。
if (hasUnqualifiedResourceBindings()) {
// Aapt can change IDs out from underneath us,just suppress since all will work at runtime.
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value","$S","ResourceType")
.build());
}
// 有onTouch注解时,增加@SuppressLint("ClickableViewAccessibility")
if (hasOnTouchMethodBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
.addMember("value","ClickableViewAccessibility")
.build());
}
// 调用父类构造函数
if (parentBinding != null) {
if (parentBinding.constructorNeedsView()) {
constructor.addStatement("super(target,source)");
} else if (constructorNeedsView()) {
constructor.addStatement("super(target,source.getContext())");
} else {
constructor.addStatement("super(target,context)");
}
constructor.addCode("\n");
}
if (hasTargetField()) {
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
// View绑定
if (hasViewBindings()) {
if (hasViewLocal()) { // 临时变量 View view;
// Local variable in which all views will be temporarily stored.
constructor.addStatement("$T view",VIEW);
}
// 单个view处理,findView、onclick等
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor,binding);
}
// BindViews
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L",binding.render());
}
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
// 资源绑定
if (!resourceBindings.isEmpty()) {
if (constructorNeedsView()) {
constructor.addStatement("$T context = source.getContext()",CONTEXT);
}
if (hasResourceBindingsNeedingResource(sdk)) {
constructor.addStatement("$T res = context.getResources()",RESOURCES);
}
for (ResourceBinding binding : resourceBindings) {
constructor.addStatement("$L",binding.render(sdk));
}
}
return constructor.build();
}
addViewBinding方法进行了findView的操作和setOnClickListener等监听操作。具体生成后的代码如下所示:
view = Utils.findrequiredView(source,"field 'tvTitle' and method 'titleClick'");
target.tvTitle = Utils.castView(view,TextView.class);
view2131034112 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.titleClick();
}
});
① 遍历Map<TypeElement,BindingSet>,根据TypeElement(MainActivity)利用Filer工具类来生成文件TypeElement_ViewBinding(MainActivity_ViewBinding)。
② TypeElement_ViewBinding实现Unbinder接口,在unbind方法内进行解除绑定等操作,例如setOnclickListener(null)等。
③ 在构造函数内,完成注入等操作,例如findViewById,setOnclickListener,资源绑定等操作,若有父类,则会调用父类构造函数。
讲到这里,关于butterknife的分析就结束了。在分析时,最好结合生成后的代码进行分析,这样清晰很多。
后记
学习的过程总是循序渐进的,你们可以发现我学习butterknife源码的过程中也总结了两篇注解相关的文章。学习过程如下面的思维图所示: