0x01 前言 在程序开发的过程中,总会有一些场景需要去写重复冗余的代码,所以就出现了很多可以减少重复工作的框架或者工具。比如今天要分析的主角—— ButterKnife ,如果你做 Android 开发却没有听说过 ButterKnife 那就 Out 啦。ButterKnife 使用依赖注入的方式来减少程序员去编写一堆 findViewById
的代码,使用起来很方便。那么接下来就一步步地带你深入理解 ButterKnife 框架。
0x02 ButterKnife 的使用方法 我们先讲下 ButterKnife 的使用方法:
在 app/build.gradle
中添加依赖:
dependencies {
compile 'com.jakewharton:butterknife:8.4.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}
在 Activity
中添加注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ExampleActivity extends Activity { @BindView(R.id.user) EditText username; @BindView(R.id.pass) EditText password; @OnClick(R.id.submit) public void onClick (View v) { } @Override public void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this ); } }
使用方法非常简单,不得不赞叹 ButterKnife 实在是太方便了。彻底跟 findViewById
say goodbye 啦。但是我们也认识到,如果一个框架使用起来越简单,那么这个框架内部做的事情就越多。所以在 ButterKnife 内部一定做了很多事情。
今天我们主要分析下 ButterKnife 的三个部分:Annotation 、ButterKnifeProcessor 和 ButterKnife 。这三个部分就把整个 View 依赖注入的原理串联起来了。
准备好了吗?下面我们就一探究竟。(PS:本文分析的 ButterKnife 源码为 8.4.0 版本)
0x03 Annotation 我们先来看一下其中的注解部分。ButterKnife 的注解都在 butterknife-annotations 模块下:
发现我们平时常用的 @BindView
、@OnClick
和 @OnItemClick
都在里面。我们就挑 @BindView
(路径:butterknife-annotations/butterknife/BindView.java) 来看一下:
1 2 3 4 5 6 @Retention(CLASS) @Target(FIELD) public @interface BindView { @IdRes int value () ; }
注解都是用 @interface
来表示。在 BindView 注解的上面还有 @Retention
和 @Target
。
@Retention
:表示注解的保留时间,可选值 SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为 CLASS ;
@Target
:表示可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等,未标注则表示可修饰所有。
所以我们可知,@BindView
是用来修饰 field 的,并且保留至编译时刻。内部有一个默认属性 value
,用来表示 View 的 id ,即平时程序中的 R.id.xxx
。
0x04 ButterKnifeProcessor 如果只有 @BindView
是不行的,我们还需要去解析注解。如何去解析编译时的注解呢?我们可以创建一个继承自 AbstractProcessor
的注解处理器,然后实现相关方法。在 ButterKnife 中 ButterKnifeProcessor
(路径:butterknife-compiler/butterknife/compiler/ButterKnifeProcessor.java) 就是用来解析这些注解的注解处理器。
init(ProcessingEnvironment env) 我们先来看看 ButterKnifeProcessor
中的 init(ProcessingEnvironment env)
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public synchronized void init (ProcessingEnvironment env) { super .init(env); String sdk = env.getOptions().get(OPTION_SDK_INT); if (sdk != null ) { try { this .sdk = 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) { } }
在 init
中主要根据 env
得到一些工具类。其中的 filter
主要是用来生成 Java 代码,而 elementUtils
和 typeUtils
会在下面源码中用到。
getSupportedAnnotationTypes() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private static final List<Class<? extends Annotation >> LISTENERS = Arrays.asList( OnCheckedChanged.class, OnClick.class, OnEditorAction.class, OnFocusChange.class, OnItemClick.class, OnItemLongClick.class, OnItemSelected.class, OnLongClick.class, OnPageChange.class, OnTextChanged.class, OnTouch.class ); @Override public Set<String> getSupportedAnnotationTypes () { Set<String> types = new LinkedHashSet <>(); for (Class<? extends Annotation > annotation : getSupportedAnnotations()) { types.add(annotation.getCanonicalName()); } return types; } private Set<Class<? extends Annotation >> getSupportedAnnotations() { Set<Class<? extends Annotation >> annotations = new LinkedHashSet <>(); annotations.add(BindArray.class); annotations.add(BindBitmap.class); annotations.add(BindBool.class); annotations.add(BindColor.class); annotations.add(BindDimen.class); annotations.add(BindDrawable.class); annotations.add(BindFloat.class); annotations.add(BindInt.class); annotations.add(BindString.class); annotations.add(BindView.class); annotations.add(BindViews.class); annotations.addAll(LISTENERS); return annotations; }
getSupportedAnnotationTypes()
方法的作用就是返回该注解处理器所支持处理的注解集合。在 getSupportedAnnotations()
中我们可以看到一些熟悉的注解,比如 @BindView
、@OnClick
和 @OnItemClick
等。
process(Set<? extends TypeElement> elements, RoundEnvironment env) 接下来就是重头戏了,注解处理器中最重要的方法 process(Set<? extends TypeElement> elements, RoundEnvironment env)
。process(Set<? extends TypeElement> elements, RoundEnvironment env)
的代码看上去没几行,其实大部分都写在其他私有方法中了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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 true ; }
总体来看 process
方法就干了两件事情:
扫描所有的注解,然后生成以 TypeElement
为 key ,BindingSet
为 value 的 Map ;
根据生成的 Map ,遍历后通过 Filter 来生成对应的辅助类源码。PS:ButterKnife 使用了 JavaPoet 来生成 Java 源码。如果对 JavaPoet 不太熟悉,可以先阅读这篇文章 《javapoet——让你从重复无聊的代码中解放出来》 。
我们慢慢来看,先来分析一下 findAndParseTargets(env)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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); } } ... }
先来看关于 BindView
的那个 for 循环,它会遍历所有被 @BindView
注解的属性,然后调用 parseBindView
方法。那么我们就先看到 findAndParseTargets
的前半段,一起跟进 parseBindView
的方法中去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } 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, enclosingElement.getQualifiedName(), element.getSimpleName()); } else { error(element, "@%s fields must extend from View or be an interface. (%s.%s)" , BindView.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true ; } } if (hasError) { return ; } int id = element.getAnnotation(BindView.class).value(); BindingSet.Builder builder = builderMap.get(enclosingElement); if (builder != null ) { String existingBindingName = builder.findExistingBindingName(getId(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 = element.getSimpleName().toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); builder.addField(getId(id), new FieldViewBinding (name, type, required)); erasedTargetNames.add(enclosingElement); }
在 parseBindView
方法中基本上都加了注释,在方法的开头会对该 element
去做校验。如果校验没通过的话,就没有下面代码的什么事了。若校验通过之后,生成该 element
所在的类元素对应的 builder ,builder 中添加相应的 Field 绑定信息,最后添加到待 unbind 的序列中去。
现在,我们回过头来看看 findAndParseTargets(env)
方法的后半段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private Map<TypeElement, BindingSet> findAndParseTargets (RoundEnvironment env) { ... 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 { entries.addLast(entry); } } } return bindingMap; }
在 findAndParseTargets(env)
方法的后半段中,主要就是把之前的 builderMap
转换为了 bindingMap
并返回。
到了这里,我们把 process(Set<? extends TypeElement> elements, RoundEnvironment env)
做的第一件事情搞清楚了,下面就接着来看第二件事情了。
1 2 3 4 5 6 7 8 9 10 11 12 // 遍历 bindingMap 并且通过 Filer 生成 Java 代码 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()); } }
brewJava(int sdk) 从上面可以看到,遍历了之前得到的 bindingMap
,然后利用 binding
中的信息生成相应的 Java 源码。所以在 binding.brewJava(sdk)
这个方法是我们重点关注对象。那么就进入 BindingSet
(路径:butterknife-compiler/butterknife/compiler/BindingSet.java) 这个类中去看看吧:
1 2 3 4 5 6 JavaFile brewJava (int sdk) { return JavaFile.builder(bindingClassName.packageName(), createType(sdk)) .addFileComment("Generated code from Butter Knife. Do not modify!" ) .build(); }
brewJava(int sdk)
方法的代码竟然这么短 O_o ,就是利用了 JavaFile.builder
生成了一个 JavaFile
对象而已。但是我们发现其中有一个 createType(int sdk)
方法,隐隐约约感觉一定是这个方法在搞大事情。继续跟进去看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 { result.addSuperinterface(UNBINDER); } if (hasTargetField()) { result.addField(targetTypeName, "target" , PRIVATE); } if (!constructorNeedsView()) { result.addMethod(createBindingViewDelegateConstructor(targetTypeName)); } result.addMethod(createBindingConstructor(targetTypeName, sdk)); if (hasViewBindings() || parentBinding == null ) { result.addMethod(createBindingUnbindMethod(result, targetTypeName)); } return result.build(); }
在 createType(int sdk)
方法中,基本构建好了一个类的大概,其中对于构造器以及类似 findViewById
的操作都是在 createBindingConstructor(targetTypeName, sdk)
中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 private MethodSpec createBindingConstructor (TypeName targetType, int sdk) { MethodSpec.Builder constructor = MethodSpec.constructorBuilder() .addAnnotation(UI_THREAD) .addModifiers(PUBLIC); if (hasMethodBindings()) { constructor.addParameter(targetType, "target" , FINAL); } else { constructor.addParameter(targetType, "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 (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(); }
通过上面的代码就生成了构造器,但是我们还是没有看到具体 findViewById
操作的代码。别急,这些代码都在 addViewBinding(constructor, binding)
里会看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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); }
至此,整个 ButterKnifeProcessor
解析注解、生成 Java 代码的流程就走完了。我们来看看生成的代码到底长成什么样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class MainActivity_ViewBinding <T extends MainActivity > implements Unbinder { protected T target; private View view2131427413; @UiThread public MainActivity_ViewBinding (final T target, View source) { this .target = target; View view; view = Utils.findRequiredView(source, R.id.button, "field 'button' and method 'onClick'" ); target.button = Utils.castView(view, R.id.button, "field 'button'" , Button.class); view2131427413 = view; view.setOnClickListener(new DebouncingOnClickListener () { @Override public void doClick (View p0) { target.onClick(p0); } }); target.tv = Utils.findRequiredViewAsType(source, R.id.tv, "field 'textView'" , TextView.class); } @Override @CallSuper public void unbind () { T target = this .target; if (target == null ) throw new IllegalStateException ("Bindings already cleared." ); target.button = null ; target.tv = null ; view2131427413.setOnClickListener(null ); view2131427413 = null ; this .target = null ; } }
不得不赞叹一句,JavaPoet 生成的代码跟我们手写的基本上没什么区别。JavaPoet 实在是太强大了 *^ο^* 。
0x05 ButterKnife bind() 通过之前介绍 ButterKnife 的使用方法,我们知道 View 绑定是通过调用 ButterKnife.bind()
方法来实现的。下面我们来看看其内部原理 (路径:butterknife/butterknife/ButterKnife.java) :
1 2 3 4 5 6 7 8 9 10 11 12 @NonNull @UiThread public static Unbinder bind (@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); return createBinding(target, sourceView); } @NonNull @UiThread public static Unbinder bind (@NonNull View target) { return createBinding(target, target); } ...
createBinding(@NonNull Object target, @NonNull View source) 发现 bind()
方法内都会去调用 createBinding(@NonNull Object target, @NonNull View source)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 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; } 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); } } @VisibleForTesting static final Map<Class<?>, Constructor<? extends Unbinder >> BINDINGS = new LinkedHashMap <>();@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" ); 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; }
其实 createBinding(@NonNull Object target, @NonNull View source)
方法做的事情就是根据 target
创建对应的 targetClassName_ViewBinding
。在 targetClassName_ViewBinding
的构造器中会把对应的 View 进行绑定(具体可以查看上面的 MainActivity_ViewBinding
)。而在 findBindingConstructorForClass(Class<?> cls)
方法中也使用了 Class.forName()
反射来查找 Class
,这也是无法避免的。但是仅限于一个类的第一次查找,之后都会从 BINDINGS
缓存中获取。
0x06 总结 总体来说,ButterKnife 是一款十分优秀的依赖注入框架,方便,高效,减少代码量。最重要的是解放程序员的双手,再也不用去写无聊乏味的 findViewById
了。与 ButterKnife 原理相似的,还有 androidannotations 框架。