butterknife源码分析系列:
谈一谈Java的注解
http://blog.csdn.net/u012933743/article/details/54909590
如何处理注解—反射与注解处理器
http://blog.csdn.net/u012933743/article/details/54972050
代码分析
http://blog.csdn.net/u012933743/article/details/64437988
前面两篇讲解了注解的定义,以及如何用反射与注解处理器的方法来处理注解。在第二篇文章中,我们用反射的方式模拟了butterknife的findViewById来简化代码,同时,我们也说过butterknife其实是用注解处理器来实现的。
虽然butterknife的源码解析文章已有许多,这里借其肩膀,总结总结。
上文我们详细介绍了注解处理器,这里再结合butterknife再次强调下。在编译源文件时,会分析扫描注解,当扫描到butterknife定义的@BindView、@OnClick等注解时,会使用JavaPoet来生成代码。生成后的文件会再次分析,直到没有分析到需要处理的注解位置。
Poet译为诗人,JavaPoet可以帮助便捷地生成代码,而不是手动繁琐的拼接语句。简要介绍下比较关键的几个类:
- MethodSpec 代表一个构造函数或方法声明。
- TypeSpec 代表一个类,接口,或者枚举声明。
- FieldSpec 代表一个成员变量,一个字段声明。
详细的使用方法可以看github上的介绍:
https://github.com/square/javapoet/
直接从源码分析容易一头雾水。既然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, R.id.tv_title, "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, "field 'tvTitle'", TextView.class);
利用Utils.findRequiredViewAsType得到的结果赋值到我们定义的TextView(tvTitle)上,这也解释了为什么tvTitle需要用public来修饰。
进一步看看findRequiredViewAsType。
public static T findRequiredViewAsType(View source, @IdRes int id, String who,
Class cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, 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偷偷在注解处理器里做了什么。
关于注解处理器:
http://blog.csdn.net/u012933743/article/details/54972050
注解处理器里包含下面几个重要的方法:
- init()
初始化,得到Elements、Types、Filer等工具类- getSupportedAnnotationTypes()
描述注解处理器需要处理的注解- process()
扫描分析注解,生成代码
从process方法入手:
@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
Map bindingMap = findAndParseTargets(env);
// 利用JavaPoet生成代码
for (Map.Entry 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
获得TypeElement -> BindingSet的映射关系,TypeElement指的是类或接口,在本文所举的栗子中是MainActivity。BindingSet里包含了生成代码时的一些参数。- 运用JavaPoet框架来生成代码
生成的代码类形式为xxxx_ViewBinding
跟踪进入findAndParseTargets方法。
private Map findAndParseTargets(RoundEnvironment env) {
Map builderMap = new LinkedHashMap<>();
Set 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转化为Map
Deque> entries =
new ArrayDeque<>(builderMap.entrySet());
Map bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry 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 builderMap,
Set 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)",
BindView.class.getSimpleName(), qualifiedName, 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)",
BindView.class.getSimpleName(), id, 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
Deque> entries =
new ArrayDeque<>(builderMap.entrySet());
Map bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry 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);
}
}
}
遍历builderMap,先通过findParentType判断是否有父类,若有,通过setParent来设置进去。在生成代码时,若有父类,会把父类的注解也注入。findParentType方法是通过TypeMirror来获取父类的信息(TypeMirror可以获取类里面的方法、域、超类等信息)。
在生成Map
后,下一步就是使用JavaPoet框架生成相关的代码。
@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
Map bindingMap = findAndParseTargets(env);
for (Map.Entry 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;
}
遍历Map,根据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, "target", 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", "$S", "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, R.id.tv_title, "field 'tvTitle' and method 'titleClick'");
target.tvTitle = Utils.castView(view, R.id.tv_title, "field 'tvTitle'", TextView.class);
view2131034112 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.titleClick();
}
});
总结下在使用JavaPoet生成代码的步骤:
① 遍历Map,根据TypeElement(MainActivity)利用Filer工具类来生成文件TypeElement_ViewBinding(MainActivity_ViewBinding)。
② TypeElement_ViewBinding实现Unbinder接口,在unbind方法内进行解除绑定等操作,例如setOnclickListener(null)等。
③ 在构造函数内,完成注入等操作,例如findViewById,setOnclickListener,资源绑定等操作,若有父类,则会调用父类构造函数。
讲到这里,关于butterknife的分析就结束了。在分析时,最好结合生成后的代码进行分析,这样清晰很多。
学习的过程总是循序渐进的,你们可以发现我学习butterknife源码的过程中也总结了两篇注解相关的文章。学习过程如下面的思维图所示:
若有疑问或错误,请在评论中写出,谢谢!