作为Android应用开发工程师估计没有人没听过Butterknife吧,我们可以借助它来简化view的查找过程以及通过注释将监听器绑定到事件响应方法上,这样可以减少很多代码量。对于Butterknife的用法在之前的博客中已经做了相应的介绍,这部分主要是分析Butterknife的源码,在之后的博文中还会对编译时注解解析器的实现做对应的介绍,如果想要对这部分内容有比较深入的了解,可以看下 《Java 进阶系列之注解》 以及《一步一步编写编译时注解框架》以及该篇博客。
好了言归正传,我们在介绍之前看下Butterknife 的工作原理: 在没有Butterknife的情况下我们用的是 Android 原生的API方法findViewById,而有了Butterknife后我们就可以直接使用@BindView(R.id.text) , 但是如果仔细看过源码的同学,会发现到最后还是调用findViewById,只不过这个是编译时注解框架帮我们生成了对应的代码,换句话说Butterknife以带有注解的源码文件作为javac编译器输入,在编译的时候会调用注解解析工具,对这些源码中的注解进行解析,解析后再通过代码生成工具来产生对应的源码文件,最后将我们自己的源码文件和注解解析工具产生的源码一同作为编译器输入进行编译。
ok 了解了整个原理我们就可以进行深入研究了。
我们将分成两大部分进行分析:
[项目结构]:
[绑定过程]: 在绑定实用的是bind方法,bing方法实现如下,它的实现十分简单,但是在介绍bind时候,必须要先大概知道什么是target,什么是source,简单讲target就是一个实体,它承载着source,目前butterknife支持的target 包括activity,dialog,以及view。source就是我们实际要查找控件的地方。也就是findViewById 应用的对象。下面是以activity为target,所以对应的source就是对应activity的DecorView.
@NonNull @UiThread public static Unbinder bind (@NonNull Activity target ) { View sourceView = target .getWindow().getDecorView(); return createBinding (target , sourceView) ; }
接着我们来看下createBinding方法,在方法开始的时候会使用target的class对象来获取对应的ViewBinding的构造方法,target的ViewBinding对象是由ButterknifeProcessor生成的,这个会在后面介绍ButterknifeProcessor的时候进行详细展开介绍。获取到对应的ViewBinding对象后调用该对象的构造方法并返回。
private static Unbinder createBinding (@NonNull Object target, @NonNull View source ) { Class <?> targetClass = target.getClass (); Constructor <? extends Unbinder > constructor = findBindingConstructorForClass (targetClass); if (constructor == null ) { return Unbinder .EMPTY ; } 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); } }
findBindingConstructorForClass 的作用就是加载对应targetClass的ViewBinding对象的构造方法:
@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 = cls.getClassLoader() .loadClass(clsName + "_ViewBinding" ) ; 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; }
为了提高整体性能,这里使用了BINDINGS这个Map作为内存缓存。BINDINGS的定义如下:
static final Map<Class <?>, Constructor <? extends Unbinder >> BINDINGS = new LinkedHashMap <>() ;
它的键值为我们的target 的类对象,我们以MainActivity为例子,那么key为MainActivity.class 对应的值呢?就是MainActivity_ViewBinding的构造方法,这个构造方法只有一个参数(View). 这个阶段的整个过程如下: 拿着target类对象到BINDINGS缓存中去匹配,看下是否有缓存,如果有缓存那么使用缓存,如果没有那么就获取JavaPoet产生的MainActivity_ViewBinding(View view)的构造方法,并添加到缓冲中,供后续使用,为啥要缓存? 因为这里获取MainActivity_ViewBinding的构造方法使用的是反射的方式,对性能有所影响,使用缓存可以减少反射的使用次数,但是这里有一个疑问就是会不会因为Butterknife的大量使用从而导致缓存占用很大的空间,这个问题又是如何克服的?我们带着这个问题继续。获取到构造方法后我们new出一个实例对象,这里可以看出 MainActivity_ViewBinding.java继承的是Unbinder,而Unbinder中的unbind方法有可能就是处理缓存清除的工作。 我们看下Butterknife sample例子中产生的一个代码:
public class G_ViewBinding<T extends G> extends E_ViewBinding<T> { private View view16908290; @UiThread public G_ViewBinding(final T target , View source ) { super(target, source); View view; target.button2 = Utils . findRequiredView(source , android .R.id .button2 , "field 'button2'" ) ; view = Utils . findRequiredView(source , android .R.id .content , "method 'onClick'" ) ; view16908290 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void do Click(View p0 ) { target.onClick() ; } }); Context context = source.getContext() ; Resources res = context.getResources() ; Resources.Theme theme = context.getTheme() ; target.grayColor = Utils . getColor(res , theme , android .R.color .darker_gray ) ; } @Override public void unbind() { T target = this.target; super.unbind() ; target.button2 = null; view16908290.setOnClickListener(null ) ; view16908290 = null; } }
我们先不看上面的具体代码,只看unbind方法。它会递归地将对应的view对象设置成null,从而在gc的时候释放掉资源。
从上面代码看整个butterknife包的源码是十分简单的,最重要的在于xxxx_ViewBinding.class的生成,这就需要对butterknife-compiler包中的源码进行研究了,我们所有的注解都在butterknife-annotations这个包中,我们这里只以BindView注解作为分析对象: 具体的注解相关内容见《Java进阶之注解》 这篇博客。
@Retention (CLASS) @Target (FIELD)public @interface BindView { @IdRes int value(); }
从上面我们可以看出@BindView注解应用于属性定义中,并且只在源码级别以及class级别中存在,它的值为一个id类型的值。不允许其他类型的值作为参数。
[Butterknife注释解析器ButterknifeProcessor]: 从上面一小结介绍可以看出Butterknife.java 中的工作很简单就是拿着targetClass找到对应的ViewBinding,至于ViewBinding怎么来的则是这一小结的介绍重点,在介绍ButterKnife注解解析器之前我们再次回顾下注解解析器在整个编译过程中的的作用: 我们知道Java使用Javac处理编译过程的,在Java的编译时期,Javac会先调用java注解处理器来进行处理。因此我们可以自定义注解处理器来做一些处理。注解处理器通常以java源码或者已编译的字节码作为输入,然后生成一些源码文件作为输出。在这个过程可以在已有的代码上添加一些方法,来帮我们做一些有用的事情。这些生成的 java 文件跟其他手动编写的java源码一样,会被 javac 编译。编译时解析是注解强大的地方之一。我们可以利用它在编译时帮你生成一些代码逻辑,避免了运行时利用反射解析所带来的性能开销。
下面是ButterKnifeProcessor的大体结构:
@AutoService (Processor .class )public final class ButterKnifeProcessor extends AbstractProcessor { @Override public SourceVersion getSupportedSourceVersion ( ) { return SourceVersion .latestSupported (); } @Override public Set <String > getSupportedOptions ( ) { return Collections .singleton (OPTION_SDK_INT ); } public synchronized void init (ProcessingEnvironment env ) { } @Override public Set <String > getSupportedAnnotationTypes ( ) { Set <String > types = new LinkedHashSet <>(); for (Class <? extends Annotation > annotation : getSupportedAnnotations ()) { types.add (annotation.getCanonicalName ()); } return types; } @Override public boolean process (Set <? extends TypeElement> elements, RoundEnvironment env ) { return true ; } }
下面我们就顺着这个框架对ButterKnifeProcessor进行分析: 我们首先看下 @AutoService(Processor.class)的作用:一旦在Processor类上加上注解@AutoService(Processor.class),它自动帮你将这个类添加到META-INF/service/javax.naaotation.processing.Processor下Java的ServiceLoader会自己去找到这个类并进行编译处理。 接着是init方法: init方法是在Processor创建时被javac调用并执行初始化操作。通过ProcessingEnvironment参数我们可以获得如下元素: Messager :用于打印Log,在编译的时候会在Message窗口中输出。 Elements :用于处理编程元素的辅助类 Filer :用于注解处理器生成新文件的 Locale :用于获取时区信息的类 Types :用于操作类型的辅助方法 其中最常用的就是Elements以及Types和打印Log用到的Messager.
@Override public synchronized void init (ProcessingEnvironment env) { super .init(env); String sdk = env .getOptions().get(OPTION_SDK_INT); if (sdk != null ) { try { this .sd k = Integer.parseInt(sdk); } catch (NumberFormatException e) { env.getMessager().printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '" + sdk + "'. Falling back to API 1 support." ); } } elementUtils = env.getElementUtils(); typeUtils = env.getTypeUtils(); filer = env.getFiler(); try { trees = Trees.instance(processingEnv); } catch (IllegalArgumentException ignored) { } }
接着就是整个注解解析器的关键方法process,每次执行时,处理器方法被调用,并且传入了当前要处理的注解类型。可以在这个方法中扫描和处理注解,并生成Java代码。我们看下面的process方法,在这个方法中主要完成了两项任务,一个就是调用findAndParseTargets来获取bindingMap bindingMap 是什么呢?它其实对应的是每个有调用bind绑定的target,举个例子来说,我们在MainActicity的onCreate方法调用了Butterknife的bind方法,那么ButterknifeProcessor就会产生一个对应的BindingSet,也就是说有多少个bind,bindingMap里面就有多少个BindingSet。然后再使用Javapost来为每个BingSet生成一个_ViewBinding.class.
@Override public boolean process (Set <? extends TypeElement> elements, RoundEnvironment env ) { Map <TypeElement , BindingSet > bindingMap = findAndParseTargets (env); 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 (IOExceptio n e) { error (typeElement, "Unable to write binding for type %s: %s" , typeElement, e.getMessage ()); } } return false ; }
我们先来看下findAndParseTarets,它的任务分成如下几个部分:
调用scanForRClasses 分别通过RClassScanner IdScanner VarScanner 解析R 文件并将解析结果存放到symbols 中,至于symbols的作用是什么我们后面介绍
调用一系列parseXXXX方法解析各种注解
调用findAndParseListener 来解析各种事件注解
整理BindingSet的继承关系private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env ) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>() ; Set<TypeElement> erasedTargetNames = new LinkedHashSet<>() ; scanForRClasses(env ) ; for (Element element : env.getElementsAnnotatedWith(BindView.class ) ) { try { parseBindView(element , builderMap , erasedTargetNames ) ; } catch (Exception e) { logParsingError(element , BindView.class , e ) ; } } for (Class<? extends Annotation> listener : LISTENERS) { findAndParseListener(env , listener , builderMap , erasedTargetNames ) ; } 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) { bindin gMap.put(type , builder.build() ); } else { BindingSet parentBinding = bindingMap.get(parentType); if (parentBinding != null) { builder.setParent(parentBinding ) ; bindingMap.put(type , builder.build() ); } else { entries.addLast(entry ) ; } } } return bindingMap; }
首先看下scanForRClasses: 它的作用是解析R文件将获取到的数据存入symbols,用于作为后续标识每个元素的key。R 元素中的每一个变量对应symols中的一个元素。
private void scanForRClasses(RoundEnvironment env ) { if (trees == null) return; RClassScanner scanner = new RClassScanner() ; for (Class<? extends Annotation> annotation : getSupportedAnnotations() ) { for (Element element : env.getElementsAnnotatedWith(annotation ) ) { JCTree tree = (JCTree) trees.getTree(element , getMirror (element , annotation ) ); if (tree != null) { String respectivePackageName = elementUtils.getPackageOf(element ) .getQualifiedName() .to String() ; scanner.setCurrentPackageName(respectivePackageName ) ; tree.accept(scanner); } } } for (Map.Entry<String, Set<String>> packageNameToRClassSet : scanner.getRClasses() .entrySet() ) { String respectivePackageName = packageNameToRClassSet.getKey() ; for (String rClass : packageNameToRClassSet.getValue() ) { parseRClass(respectivePackageName / * key * / , rClass / * value * / ) ; } } }
查找R文件使用的是RClassScanner,它是一个TreeScanner,它会找到整个项目的R文件:
private static class RClassScanner extends TreeScanner { private final Map <String , Set <String >> rClasses = new LinkedHashMap <>(); private String currentPackageName; @Override public void visitSelect (JCTree.JCFieldAccess jcFieldAccess ) { Symbol symbol = jcFieldAccess.sym ; if (symbol != null && symbol .getEnclosingElement () != null && symbol .getEnclosingElement ().getEnclosingElement () != null && symbol .getEnclosingElement ().getEnclosingElement ().enclClass () != null ) { Set <String > rClassSet = rClasses.get (currentPackageName); if (rClassSet == null ) { rClassSet = new HashSet <>(); rClasses.put (currentPackageName, rClassSet); } rClassSet.add (symbol .getEnclosingElement ().getEnclosingElement ().enclClass ().className ()); } } Map <String , Set <String >> getRClasses ( ) { return rClasses; } void setCurrentPackageName (String respectivePackageName ) { this .currentPackageName = respectivePackageName; } }
在找到全部的R文件后在scanForRClass中会调用parseRClass对扫描到的R文件进行解析:
private void parseRClass (String respectivePackageName, String rClass ) { Element element; try { element = elementUtils.getTypeElement (rClass); } catch (MirroredTypeException mte) { element = typeUtils.asElement (mte.getTypeMirror ()); } JCTree tree = (JCTree ) trees.getTree (element); if (tree != null ) { IdScanner idScanner = new IdScanner (symbols, elementUtils.getPackageOf (element).getQualifiedName ().toString (), respectivePackageName); tree.accept (idScanner); } else { parseCompiledR (respectivePackageName, (TypeElement ) element); } }
我们从上面代码中可以看到在parseRClass中会使用IdScanner 对当前扫描的R文件进行扫描:
private static class IdScanner extends TreeScanner { private final Map <QualifiedId , Id > ids; private final String rPackageName; private final String respectivePackageName; IdScanner (Map <QualifiedId , Id > ids, String rPackageName, String respectivePackageName) { this .ids = ids; this .rPackageName = rPackageName; this .respectivePackageName = respectivePackageName; } @Override public void visitClassDef(JCTree .JCClassDecl jcClassDecl) { for (JCTree tree : jcClassDecl.defs) { if (tree instanceof ClassTree ) { ClassTree classTree = (ClassTree ) tree; String className = classTree.getSimpleName().toString(); if (SUPPORTED_TYPES .contains(className)) { ClassName rClassName = ClassName .get(rPackageName, "R" , className); VarScanner scanner = new VarScanner (ids, rClassName , respectivePackageName); ((JCTree ) classTree).accept(scanner); } } } } }
在IdScanner 中会对R文件中的每个内部类进行扫描,判断当前的内部类是否是所支持的,如果是那么就调用VarScanner进行进一步扫描。
private static class VarScanner extends TreeScanner { private final Map<QualifiedId, Id> ids; private final ClassName className; private final String respectivePackageName; private VarScanner(Map<QualifiedId, Id> ids , ClassName className / * com .demo .example .R.anim * / , String respectivePackageName ) { this.ids = ids; this.className = className; this.respectivePackageName = respectivePackageName; } @Override public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl ) { if ("int" .equals(jcVariableDecl.getType() .to String() )) { int id = Integer . valueOf(jcVariableDecl .getInitializer () .to String() ); String resourceName = jcVariableDecl.getName() .to String() ; QualifiedId qualifiedId = new QualifiedId(respectivePackageName , id ) ; ids.put(qualifiedId, new Id(id , className , resourceName ) ); } } }
VarScanner 也是一个TreeScanner,它的所用就是在R 文件内部类中扫描,并将扫描到的id 添加到symbos中。
介绍到这里我们做下简要,首先我们会通过RCScanner找到项目的R文件,再使用IdScanner对R文件中的内部类进行扫描,每个内部类对应一类资源,最后通过VarScanner对id进行扫描然后存入sybols这个map.
symbols定义如下:
private final Map <QualifiedId , Id > symbols = new LinkedHashMap <>();
--- --- --- --- --- --- . . . . ---
介绍完scanForRClass我们继续了解下注解的解析过程: 在进行解析之前会先对注解所应用的对象做初步检查:
验证对应的修饰符是否是static 或者 private ,注解是否正在修饰局部变量,是否处于私有类中,以及检查包名是否是在android以及java标准API包内,如果有上述的情况就返回false
获取使用该注解的元素对应类型是否继承自View
如果校验无误,那么会先查询bingMap中是否已经有对应的BindingSet了,如果没有就创建一个并添加到bingMap中。如果有的话就完对应的BindingSet添加Field这些Field会在JavaPost创建对应源码文件的时候用于生成对应的成员变量。这个后面会进行详细介绍。 我们下面深入看两点:
BindingSet是怎么创建的
addFeild是怎么添加的private void parseBindView(Element element , Map<TypeElement, BindingSet.Builder> builderMap , Set<TypeElement> erasedTargetNames ) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement() ; boolean hasError = isInaccessibleViaGeneratedCode(BindView.class , "fields" , element ) || isBindingInWrongPackage(BindView.class , element ) ; TypeMirror elementType = element.as Type() ; if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound() ; } Name qualifiedName = enclosingElement.getQualifiedName() ; Name simpleName = element.getSimpleName() ; 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; } int id = element.getAnnotation(BindView.class ) .value() ; QualifiedId qualifiedId = elementToQualifiedId(element , id ) ; BindingSet.Builder builder = builderMap.get(enclosingElement); if (builder != null) { String existingBindingName = builder.findExistingBindingName(getId (qualifiedId ) ); 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.to String() ; TypeName type = TypeName . get(elementType); boolean required = isFieldRequired(element ) ; builder.addField(getId (qualifiedId ) , new FieldViewBinding(name , type , required ) ); erasedTargetNames.add(enclosingElement); }
BindingSet的创建是通过调用getOrCreateBindingBuilder方法来完成的: 它会先判断当前target类型是activity,dialog,还是一般的view,以及生成target对应的ViewBinding文件名。
private BindingSet.Builder getOrCreateBindingBuilder(Map<TypeElement, BindingSet.Builder> builderMap , TypeElement enclosingElement ) { BindingSet.Builder builder = builderMap.get(enclosingElement); if (builder == null) { builder = BindingSet .new Builder(enclosingElement / * MainActivity* / ) ; builderMap.put(enclosingElement, builder); } return builder; }
static Builder new Builder(TypeElement enclosingElement ) { TypeMirror typeMirror = enclosingElement.as Type() ; boolean isView = isSubtypeOfType(typeMirror , VIEW_TYPE) ; boolean isActivity = isSubtypeOfType(typeMirror , ACTIVITY_TYPE) ; boolean isDialog = isSubtypeOfType(typeMirror , DIALOG_TYPE) ; TypeName targetType = TypeName . get(typeMirror); if (targetType instanceof ParameterizedTypeName) { targetType = ((ParameterizedTypeName) targetType).rawType; } String packageName = getPackage(enclosingElement ) .getQualifiedName() .to String() ; String className = enclosingElement.getQualifiedName() .to String() .substring(packageName.length() + 1 ).replace('.' , '$' ); ClassName bindingClassName = ClassName . get(packageName, className + "_ViewBinding" ); boolean isFinal = enclosingElement.getModifiers() .contains(Modifier.FINAL); return new Builder(targetType , bindingClassName , isFinal , isView , isActivity , isDialog ) ; }
那BindingSet的结构是怎样的呢?我们继续往下看:
static final class Builder { private final TypeName targetTypeName; private final ClassName bindingClassName; private final boolean isFinal; private final boolean isView; private final boolean isActivity; private final boolean isDialog; private BindingSet parentBinding; private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap <>(); private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings = ImmutableList.builder(); private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder(); void addField (Id id, FieldViewBinding binding) { ViewBinding.Builder viewBinding = getOrCreateViewBindings(id); viewBinding.setFieldBinding(binding); } boolean addMethod (Id id, ListenerClass listener, ListenerMethod method, MethodViewBinding binding) { ViewBinding.Builder viewBinding = getOrCreateViewBindings(id); if (viewBinding.hasMethodBinding(listener, method) && !"void" .equals(method.returnType())) { return false ; } viewBinding.addMethodBinding(listener, method, binding); return true ; } void addFieldCollection (FieldCollectionViewBinding binding) { collectionBindings.add(binding); } void addResource (ResourceBinding binding) { resourceBindings.add(binding); } void setParent (BindingSet parent) { this .parentBinding = parent; } String findExistingBindingName (Id id) { ViewBinding.Builder builder = viewIdMap.get(id); if (builder == null ) { return null ; } FieldViewBinding fieldBinding = builder.fieldBinding; if (fieldBinding == null ) { return null ; } return fieldBinding.getName(); } 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<ViewBinding> viewBindings = ImmutableList.builder(); for (ViewBinding.Builder builder : viewIdMap.values()) { viewBindings.add(builder.build()); } return new BindingSet (targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog, viewBindings.build(), collectionBindings.build(), resourceBindings.build(), parentBinding); } }
我们从BindingSet的Builder可以看出它有如下的成员变量:
parentBinding 指向父类的BindingSet。 viewIdMap 与View 相关的绑定信息,包括view 对象以及事件方法。 collectionBindings 多个view 集体绑定的情况 resourceBindings 资源绑定
它有如下方法:
void addField(Id id, FieldViewBinding binding) 添加Field boolean addMethod(Id id, ListenerClass listener, ListenerMethod method , MethodViewBinding binding ) 添加事件方法 void addFieldCollection (FieldCollectionViewBinding binding) 添加Field 集合void addResource (ResourceBinding binding) 添加资源
介绍完BindView 我也把事件绑定的方法贴出来了,下面有较为详细的注释。
private void findAndParseListener (RoundEnvironment env, Class<? extends Annotation> annotationClass, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { for (Element element : env.getElementsAnnotatedWith(annotationClass)) { if (!SuperficialValidation.validateElement(element)) continue ; try { parseListenerAnnotation(annotationClass, element, builderMap, erasedTargetNames); } catch (Exception e) { StringWriter stackTrace = new StringWriter (); e.printStackTrace(new PrintWriter (stackTrace)); error(element, "Unable to generate view binder for @%s.\n\n%s" , annotationClass.getSimpleName(), stackTrace.toString()); } } } private void parseListenerAnnotation (Class<? extends Annotation> annotationClass, Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) throws Exception { if (!(element instanceof ExecutableElement) || element.getKind() != METHOD) { throw new IllegalStateException (String.format("@%s annotation must be on a method." , annotationClass.getSimpleName())); } ExecutableElement executableElement = (ExecutableElement) element; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); Annotation annotation = element.getAnnotation(annotationClass); Method annotationValue = annotationClass.getDeclaredMethod("value" ); if (annotationValue.getReturnType() != int [].class) { throw new IllegalStateException (String.format("@%s annotation value() type not int[]." , annotationClass)); } int [] ids = (int []) annotationValue.invoke(annotation); String name = executableElement.getSimpleName().toString(); boolean required = isListenerRequired(executableElement); boolean hasError = isInaccessibleViaGeneratedCode(annotationClass, "methods" , element); hasError |= isBindingInWrongPackage(annotationClass, element); Integer duplicateId = findDuplicate(ids); if (duplicateId != null ) { error(element, "@%s annotation for method contains duplicate ID %d. (%s.%s)" , annotationClass.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true ; } ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); if (listener == null ) { throw new IllegalStateException ( String.format("No @%s defined on @%s." , ListenerClass.class.getSimpleName(), annotationClass.getSimpleName())); } ListenerMethod method; ListenerMethod[] methods = listener.method(); if (methods.length > 1 ) { throw new IllegalStateException (String.format("Multiple listener methods specified on @%s." , annotationClass.getSimpleName())); } else if (methods.length == 1 ) { if (listener.callbacks() != ListenerClass.NONE.class) { throw new IllegalStateException (String.format("Both method() and callback() defined on @%s." , annotationClass.getSimpleName())); } method = methods[0 ]; } else { Method annotationCallback = annotationClass.getDeclaredMethod("callback" ); Enum<?> callback = (Enum<?>) annotationCallback.invoke(annotation); Field callbackField = callback.getDeclaringClass().getField(callback.name()); method = callbackField.getAnnotation(ListenerMethod.class); if (method == null ) { throw new IllegalStateException ( String.format("No @%s defined on @%s's %s.%s." , ListenerMethod.class.getSimpleName(), annotationClass.getSimpleName(), callback.getDeclaringClass().getSimpleName(), callback.name())); } } List<? extends VariableElement > methodParameters = executableElement.getParameters(); if (methodParameters.size() > method.parameters().length) { error(element, "@%s methods can have at most %s parameter(s). (%s.%s)" , annotationClass.getSimpleName(), method.parameters().length, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true ; } TypeMirror returnType = executableElement.getReturnType(); if (returnType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) returnType; returnType = typeVariable.getUpperBound(); } if (!returnType.toString().equals(method.returnType())) { error(element, "@%s methods must have a '%s' return type. (%s.%s)" , annotationClass.getSimpleName(), method.returnType(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true ; } if (hasError) { return ; } Parameter[] parameters = Parameter.NONE; MethodViewBinding binding = new MethodViewBinding (name, Arrays.asList(parameters), required); BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement); for (int id : ids) { QualifiedId qualifiedId = elementToQualifiedId(element, id); if (!builder.addMethod(getId(qualifiedId), listener, method, binding)) { error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)" , id, enclosingElement.getQualifiedName(), element.getSimpleName()); return ; } } erasedTargetNames.add(enclosingElement); }
使用JavaPoest为每个BindingSet创建对应的_ViewBinding文件: 我们在此回到process方法
@Override public boolean process (Set <? extends TypeElement> elements, RoundEnvironment env ) { Map <TypeElement , BindingSet > bindingMap = findAndParseTargets (env); 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 ; }
我们上面了解了如何从R文件中获取对应的信息构成后续作为Id使用的symbols。再对代码中使用到的Butterknife注解进行解析,获取到对应的View 绑定,事件方法绑定,资源绑定信息,添加到对应BingSet中,最后我们要了解的是最后一步 – 使用JavaPost来将BindingSet中收集到的View 绑定,事件方法绑定,资源绑定信息生成对应的源码: 我们先来看下brewJava 方法:
JavaFile brewJava(int sdk ) { return JavaFile .builder(bindingClassName.packageName() , createType(sdk ) ) .addFileComment("Generated code from Butter Knife. Do not modify!" ) .build() ; }
brewJava 方法中最重要的就是createType,它是生成_ViewBinding的核心代码,下面给出了详细的注释:
private TypeSpec createType(int sdk ) { TypeSpec.Builder result = TypeSpec .class Builder(bindingClassName .simpleName () ).addModifiers(PUBLIC) ; if (isFinal) { result.addModifiers(FINAL) ; } if (parentBinding != null) { result.superclass(parentBinding.bindingClassName); } else { result.addSuperinterface(UNBINDER) ; } if (hasTargetField() ) { result.addField(targetTypeName , "target" , PRIVATE) ; } if (isView) { result.addMethod(createBindingConstructorForView () ); } else if (isActivity) { result.addMethod(createBindingConstructorForActivity () ); } else if (isDialog) { result.addMethod(createBindingConstructorForDialog () ); } if (!constructorNeedsView() ) { result.addMethod(createBindingViewDelegateConstructor () ); } result.addMethod(createBindingConstructor (sdk ) ); if (hasViewBindings() || parentBinding == null) { result.addMethod(createBindingUnbindMethod (result ) ); } return result.build() ; }
那么我们在Butterknife bind方法中调用的构造函数是哪个呢:就是createBindingConstructor
private MethodSpec createBindingConstructor(int sdk ) { MethodSpec.Builder constructor = MethodSpec . constructorBuilder() .addAnnotation(UI_THREAD) .addModifiers(PUBLIC) ; if (hasMethodBindings() ) { constructor.addParameter(targetTypeName , "target" , FINAL) ; } else { constructor.addParameter(targetTypeName , "target" ) ; } if (constructorNeedsView() ) { constructor.addParameter(VIEW, "source" ) ; } else { constructor.addParameter(CONTEXT, "context" ) ; } if (hasUnqualifiedResourceBindings() ) { constructor.addAnnotation(AnnotationSpec.builder (SuppressWarnings.class ) .addMember("value" , "$S" , "ResourceType" ) .build() ); } 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" ) ; } if (hasViewBindings() ) { if (hasViewLocal() ) { constructor.addStatement("$T view" , VIEW) ; } for (ViewBinding binding : viewBindings) { addViewBinding(constructor , binding ) ; } 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,在调用addViewBinding之前会调用hasViewBindings来判断当前是否有view绑定以及事件方法绑定,如果有才会调用addViewBinding. 在addViewBinding方法中会判断是否isRequired来决定是调用butterknife.internal.Utils的findRequiredViewAsType 还是findOptionalView
private void addViewBinding (MethodSpec.Builder result, ViewBinding binding) { if (binding.isSingleFieldBinding ()) { FieldViewBinding fieldBinding = binding.getFieldBinding (); CodeBlock.Builder builder = CodeBlock.builder ().add ("target.$L = " , fieldBinding.getName ()); boolean requiresCast = requiresCast (fieldBinding.getType ()); if (!requiresCast && !fieldBinding.isRequired ()) { builder.add ("source.findViewById($L )" , binding.getId ().code); } else { builder.add ("$T .find" , UTILS); builder.add (fieldBinding.isRequired () ? "RequiredView" : "OptionalView" ); if (requiresCast) { builder.add ("AsType" ); } builder.add ("(source, $L " , binding.getId ().code); if (fieldBinding.isRequired () || requiresCast) { builder.add (", $S " , asHumanDescription (singletonList (fieldBinding))); } if (requiresCast) { builder.add (", $T .class" , fieldBinding.getRawType ()); } builder.add (")" ); } result.addStatement ("$L " , builder.build ()); return ; } List<MemberViewBinding> requiredBindings = binding.getRequiredBindings (); if (requiredBindings.isEmpty ()) { result.addStatement ("view = source.findViewById($L )" , binding.getId ().code); } else if (!binding.isBoundToRoot ()) { result.addStatement ("view = $T .findRequiredView(source, $L , $S )" , UTILS, binding.getId ().code, asHumanDescription (requiredBindings)); } addFieldBinding (result, binding); addMethodBindings (result, binding); }
findOptionalViewAsType 和 findRequiredViewAsType 有什么区别呢?我们从下面代码可以看出findRequiredViewAsType会在source中获取对应的控件,后判断是否获取到,如果没有获取到会抛出如下异常:
throw new IllegalStateException ("Required view '" + name + "' with ID " + id + " for " + who + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'" + " (methods) annotation." );
而最终的类型转换是通过cls.cast 方法来完成的。
public static <T> T findOptionalViewAsType (View source, @IdRes int id, String who, Class<T> cls ) { View view = source.findViewById (id); return castView (view, id, who, cls); } public static <T> T findRequiredViewAsType (View source, @IdRes int id, String who, Class<T> cls ) { View view = findRequiredView (source, id, who); return castView (view, id, who, cls); } public static View findRequiredView (View source, @IdRes int id, String who ) { View view = source.findViewById (id); if (view != null ) { return view; } String name = getResourceEntryName (source, id); throw new IllegalStateException ("Required view '" + name + "' with ID " + id + " for " + who + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'" + " (methods) annotation." ); } public static <T> T castView (View view, @IdRes int id, String who, Class<T> cls ) { try { return cls.cast (view); } catch (ClassCastException e) { String name = getResourceEntryName (view, id); throw new IllegalStateException ("View '" + name + "' with ID " + id + " for " + who + " was of the wrong type. See cause for more info." , e); } }
到此为止整个Butterknife源码分析完毕。老规矩上图: