为什么要写这一系列的博客呢?

因为在 Android 开发的过程中, 泛型,反射,注解这些知识进场会用到,几乎所有的框架至少都会用到上面的一两种知识,如 Gson 就用到泛型,反射,注解,Retrofit 也用到泛型,反射,注解 。学好这些知识对我们进阶非常重要,尤其是阅读开源框架源码或者自己开发开源框架。

前言

ButterKnife 这个开源库火了有一段时间了,刚开始它的实现原理是使用反射实现的,性能较差。再后面的 版本中逐渐使用注解+放射实现,性能提高了不少。

ButterKnife是基于编译时的框架,它能够帮助我们减去每次写 FindViewById 的麻烦,截止到 2017.5.1 ,在 github 上面的 start 已经超过 15000.

本篇博客要分析的 ButterKnife 的源码主要包括以下三个部分,版本号是8.5.1

  • butterknife-annotations
  • butterknife-compiler
  • butterknife

其中 butterknife-annotations 库主要用来存放自定义注解;butterknife-compiler 主要是用来扫描哪些地方使用到我们的自定义注解,并进行相应的处理,生成模板代码等;butterknife 主要是用来注入我们的代码的。

我们先来先一下要怎样使用 butterknife:

ButterKnife 的基本使用

在 moudle 的 build.gradle 增加依赖

dependencies {compile 'com.jakewharton:butterknife:8.5.1'annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}
public class SimpleActivity extends Activity {private static final ButterKnife.Action<View> ALPHA_FADE = new ButterKnife.Action<View>() {@Override public void apply(@NonNull View view, int index) {AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);alphaAnimation.setFillBefore(true);alphaAnimation.setDuration(500);alphaAnimation.setStartOffset(index * 100);view.startAnimation(alphaAnimation);}};@BindView(R2.id.title) TextView title;@BindView(R2.id.subtitle) TextView subtitle;@BindView(R2.id.hello) Button hello;@BindView(R2.id.list_of_things) ListView listOfThings;@BindView(R2.id.footer) TextView footer;@BindViews({ R2.id.title, R2.id.subtitle, R2.id.hello }) List<View> headerViews;private SimpleAdapter adapter;@OnClick(R2.id.hello) void sayHello() {Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show();ButterKnife.apply(headerViews, ALPHA_FADE);}@OnLongClick(R2.id.hello) boolean sayGetOffMe() {Toast.makeText(this, "Let go of me!", LENGTH_SHORT).show();return true;}@OnItemClick(R2.id.list_of_things) void onItemClick(int position) {Toast.makeText(this, "You clicked: " + adapter.getItem(position), LENGTH_SHORT).show();}@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.simple_activity);ButterKnife.bind(this);// Contrived code to use the bound fields.title.setText("Butter Knife");subtitle.setText("Field and method binding for Android views.");footer.setText("by Jake Wharton");hello.setText("Say Hello");adapter = new SimpleAdapter(this);listOfThings.setAdapter(adapter);}
}

调用 gradle build 命令,我们在相应的目录下将可以看到生成类似这样的代码。

public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {protected T target;private View view2130968578;private View view2130968579;@UiThreadpublic SimpleActivity_ViewBinding(final T target, View source) {this.target = target;View view;target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);view2130968578 = view;view.setOnClickListener(new DebouncingOnClickListener() {@Overridepublic void doClick(View p0) {target.sayHello();}});view.setOnLongClickListener(new View.OnLongClickListener() {@Overridepublic boolean onLongClick(View p0) {return target.sayGetOffMe();}});view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);view2130968579 = view;((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {target.onItemClick(p2);}});target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);target.headerViews = Utils.listOf(Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));}@Override@CallSuperpublic void unbind() {T target = this.target;if (target == null) throw new IllegalStateException("Bindings already cleared.");target.title = null;target.subtitle = null;target.hello = null;target.listOfThings = null;target.footer = null;target.headerViews = null;view2130968578.setOnClickListener(null);view2130968578.setOnLongClickListener(null);view2130968578 = null;((AdapterView<?>) view2130968579).setOnItemClickListener(null);view2130968579 = null;this.target = null;}
}
ButterKnife 的执行流程

总的来说,大概可以分为以下几步:

  • 在编译的时候扫描注解,并做相应的处理,生成 java 代码,生成 Java 代码是调用 javapoet 库生成的。
  • 当我们调用 ButterKnife.bind(this); 方法的时候,他会根据类的全限定类型,找到相应的代码,并执行。完成 findViewById 和 setOnClick ,setOnLongClick 等操作。

第一步:在编译的时候扫描注解,并做相应的处理,生成 java 代码。这一步,可以拆分为几个小步骤:

  • 定义我们的注解,声明我们的注解是否保存到 java doc 中,可以作用于哪些区域(Filed ,Class等),以及是源码时注解,编译时注解还是运行时注解等)
  • 继承 AbstractProcessor,表示支持哪些类型的注解,支持哪些版本,
  • 重写 process 方法,处理相关的注解,存进 Map 集合中
  • 根据扫描到的注解信息(即 Map 集合),调用 javapoet 库生成 Java 代码。
butterknife-annotations 讲解


我们知道 ButterKnife 自定义很多的注解,有 BindArray,BindBitmap,BindColor,BindView 等,这里我们以 BindView 为例子讲解就 OK 了,其他的也是基本类似的,这里就不再讲解了。

//编译时注解
@Retention(CLASS)
//成员变量, (includes enum constants)
@Target(FIELD)
public @interface BindView {/** View ID to which the field will be bound. */@IdRes int value();
}
Processor 解析器说明

我们先来看一些基本方法:在 init 方法里面得到一些辅助工具类,这样有一个好处,确保工具类是单例的,因为 init 方法只会在初始化的时候调用。

public synchronized void init(ProcessingEnvironment env) {super.init(env);---//辅助工具类elementUtils = env.getElementUtils();typeUtils = env.getTypeUtils();filer = env.getFiler();---
}

接着重写 getSupportedAnnotationTypes 方法,返回我们支持的注解类型。

@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;
}

**接下来来看我们的重点, process 方法。**所做的工作大概就是拿到我们所有的注解信息,存进 map 集合,遍历 map 集合,做相应的 处理,生成 java 代码。

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {//  拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 valueMap<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);// 遍历 map 里面的所有信息,并生成 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());}}return false;
}

这里我们进入 findAndParseTargets 方法,看里面到底是怎样将注解信息存进 map 集合的?

findAndParseTargets 方法里面 针对每一个自定义注解(BindArray,BindBitmap,BindColor,BindView) 等都做了处理,这里我们重点关注 @BindView 的处理即可。其他注解的处理思想也是一样的。

我们先来看一下 findAndParseTargets 方法的前半部分,遍历 env.getElementsAnnotatedWith(BindView.class) 集合,并调用 parseBindView 方法去转化。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();scanForRClasses(env);// Process each @BindView element.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 roundstry {parseBindView(element, builderMap, erasedTargetNames);} catch (Exception e) {logParsingError(element, BindView.class, e);}}---// 后半部分,待会再讲}

可以看到牵绊部分的主要逻辑在 parseBindView 方法里面,主要做了以下几步操作:

  • 判断被注解 @BindView 修饰的成员变量是不是合法的,private 或者 static 修饰的,则出错。
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) {TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();// 判断是否被注解在属性上,如果该属性是被 private 或者 static 修饰的,则出错// 判断是否被注解在错误的包中,若包名以“android”或者“java”开头,则出错boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)|| isBindingInWrongPackage(BindView.class, element);// Verify that the target type extends from View.TypeMirror elementType = element.asType();if (elementType.getKind() == TypeKind.TYPEVAR) {TypeVariable typeVariable = (TypeVariable) elementType;elementType = typeVariable.getUpperBound();}Name qualifiedName = enclosingElement.getQualifiedName();Name simpleName = element.getSimpleName();// 判断元素是不是View及其子类或者Interfaceif (!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();// 根据所在的类元素去查找 builderBindingSet.Builder builder = builderMap.get(enclosingElement);QualifiedId qualifiedId = elementToQualifiedId(element, id);// 如果相应的 builder 已经存在if (builder != null) {// 验证 ID 是否已经被绑定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,就需要重新生成,并别存放到  builderMap 中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 方法分析完毕之后,我们在回过头来看一下 findAndParseTargets 方法的后半部分,主要做的工作是对 bindingMap 进行重排序。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {// 省略前半部分// Associate superclass binders with their subclass binders. This is a queue-based tree walk// which starts at the roots (superclasses) and walks to the leafs (subclasses).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();//获取 type 的父类的 TypeElementTypeElement parentType = findParentType(type, erasedTargetNames);// 为空,存进 mapif (parentType == null) {bindingMap.put(type, builder.build());} else {// 获取 parentType 的 BindingSetBindingSet 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;
}

到这里为止,我们已经分析完 ButterKnifeProcessor 是怎样处理注解的相关知识,并存进 map 集合中的,下面我们回到 process 方法,看一下是怎样生成 java 模板代码的。

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {//  拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 valueMap<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);// 遍历 map 里面的所有信息,并生成 java 代码for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {TypeElement typeElement = entry.getKey();BindingSet binding = entry.getValue();// 生成 javaFile 对象JavaFile javaFile = binding.brewJava(sdk);try {//  生成 java 模板代码              javaFile.writeTo(filer);} catch (IOException e) {error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());}}return false;
}

生成代码的核心代码只有这几行

// 生成 javaFile 对象
JavaFile javaFile = binding.brewJava(sdk);
try {//  生成 java 模板代码javaFile.writeTo(filer);
} catch (IOException e) {error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}

跟踪进去,发现是调用 square 公司开源的库 javapoet 开生成代码的。关于 javaPoet 的使用可以参考官网地址

JavaFile brewJava(int sdk) {return JavaFile.builder(bindingClassName.packageName(), createType(sdk)).addFileComment("Generated code from Butter Knife. Do not modify!").build();
}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);}// 如果是 View 或者是 View 的子类的话,添加构造方法if (isView) {result.addMethod(createBindingConstructorForView());} else if (isActivity) { // 如果是 Activity 或者是 Activity 的子类的话,添加构造方法result.addMethod(createBindingConstructorForActivity());} else if (isDialog) {  // 如果是 Dialog 或者是 Dialog 的子类的话,添加构造方法result.addMethod(createBindingConstructorForDialog());}//  如果构造方法不需要 View 参数,添加 需要 View 参数的构造方法if (!constructorNeedsView()) {// Add a delegating constructor with a target type + view signature for reflective use.result.addMethod(createBindingViewDelegateConstructor());}result.addMethod(createBindingConstructor(sdk));if (hasViewBindings() || parentBinding == null) {//生成unBind方法result.addMethod(createBindingUnbindMethod(result));}return result.build();
}

接着我们一起来看一下 createBindingConstructor(sdk) 方法,大概做的事情就是

  • 判断是否有设置监听,如果有监听,将 View 设置为 final
  • 遍历 viewBindings ,调用 addViewBinding 生成 findViewById 形式的代码。
private MethodSpec createBindingConstructor(int sdk) {MethodSpec.Builder constructor = MethodSpec.constructorBuilder().addAnnotation(UI_THREAD).addModifiers(PUBLIC);// 如果有方法绑定,比如 @onClick,那么增加一个 targetTypeName 类型  的方法参数 target,并且是 final 类型的if (hasMethodBindings()) {constructor.addParameter(targetTypeName, "target", FINAL);} else { // 如果没有 ,不是 final 类型的constructor.addParameter(targetTypeName, "target");}//如果有注解的 View,那么添加 VIEW 类型 source 参数if (constructorNeedsView()) {constructor.addParameter(VIEW, "source");} else {//  添加 Context 类型的 context 参数constructor.addParameter(CONTEXT, "context");}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 绑定 View,添加 @SuppressLint("ClickableViewAccessibility")if (hasOnTouchMethodBindings()) {constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT).addMember("value", "$S", "ClickableViewAccessibility").build());}// 如果 parentBinding 不为空,调用父类 的构造方法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()) {// Local variable in which all views will be temporarily stored.constructor.addStatement("$T view", VIEW);}//   遍历  viewBindings,生成  source.findViewById($L) 代码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 方法是怎样生成代码的。

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {if (binding.isSingleFieldBinding()) {// Optimize the common case where there's a single binding directly to a field.FieldViewBinding fieldBinding = binding.getFieldBinding();// 注意这里直接使用了 target. 的形式,所以属性肯定是不能 private 的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;}

ButterKnife 是怎样实现代码注入的

使用过 ButterKnife 得人基本都知道,我们是通过 bind 方法来实现注入的,即自动帮我们 findViewById ,解放我们的双手,提高工作效率。下面我们一起来看一下 bind 方法是怎样实现注入的。

@NonNull
@UiThread
public static Unbinder bind(@NonNull Activity target) {View sourceView = target.getWindow().getDecorView();return createBinding(target, sourceView);
}

可以看到 bind 方法很简单,逻辑基本都交给 createBinding 方法去完成。我们一起进入 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());// 从 Class 中查找 constructorConstructor<? 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);}
}

其实 createBinding 来说,主要做了这几件事情

  • 传入 class ,通过 findBindingConstructorForClass 方法来实例化 constructor
  • 利用反射来初始化 constructor 对象
  • 初始化 constructor 失败会抛出异常

下面我们一起来看一下 findBindingConstructorForClass 方法是怎样实现的。

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;}// 如果是 android ,java 原生的文件,不处理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");//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);}// 存进 LinkedHashMap 缓存BINDINGS.put(cls, bindingCtor);return bindingCtor;
}

它的实现思想是这样的:

  • 读取缓存,若缓存命中,直接返回,这样有利于提高效率。从代码中可以看到,缓存是通过存进 map 集合实现的。
  • 是否是我们目标文件,是的话,进行处理,不是的话,直接返回,并打印相应的日志
  • 利用类加载器加载我们自己生成的 class 文件,并获取其构造方法,获取到,直接返回。获取不到,会抛出异常,在异常的处理中,我们再从当前 class 文件的父类去查找。并把结果存进 map 集合中,做缓存处理。

我们对 ButterKnife 的分析到此为止。

题外话

这篇博客主要是分析了 ButterKnife 的主要原理实现,对 ButterKnife 里面的一些实现细节并未详细分析。不过对我们读懂代码已经足够了。下一个系列,主要讲解 CoordinatorLayout 的实现原理及怎样自定义 CoordinatorLayout 的 behavior 实现仿新浪微博发现页面的效果,敬请期待。

关于我

更多信息可以点击关于我 , 非常希望和大家一起交流 , 共同进步

分分钟带你读懂 ButterKnife 的源码相关推荐

  1. 人人都能读懂的react源码解析(大厂高薪必备)

    人人都能读懂的react源码解析(大厂高薪必备) 1.开篇(听说你还在艰难的啃react源码) ​ 本教程目标是打造一门严谨(严格遵循react17核心思想).通俗易懂(提供大量流程图解,结合demo ...

  2. 分分钟带你读懂-ButterKnife-的源码

    } }); target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", ...

  3. 播放失败246106异常代码_web前端面试题:您能读懂的Promise源码实现(手写代码)...

    Promise 是 web 前端工程师在面试的过程中很难绕过的一个坎.如果您目前处于对 Promise 一知半解,或仅仅是停留在可以使用的层面上,建议您跟着本文敲打练习一遍,相信您一定会有所收获!另外 ...

  4. web前端面试题:你能读懂的Promise源码实现(手写代码)

    Promise是web前端工程师在面试的过程中很难绕过的一个坎.如果您目前处于对Promise一知半解,或仅仅是停留在可以使用的层面上,建议您跟着本文敲打练习一遍,相信您一定会有所收获!另外文章有点长 ...

  5. java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

    java 源码系列 - 带你读懂 Reference 和 ReferenceQueue https://blog.csdn.net/gdutxiaoxu/article/details/8073858 ...

  6. 带你读懂Spring Bean 的生命周期,嘿,就是玩儿~

    带你读懂Spring Bean 的生命周期,嘿,就是玩儿~ 一.前言 今天我们来说一说 Spring Bean 的生命周期,小伙伴们应该在面试中经常遇到,这是正常现象.因为 Spring Bean 的 ...

  7. 带你读懂Spring 事务——事务的隔离级别(超详细,快藏)

    不了解事务的铁汁可以先看前两篇,讲的超详细,有问题还请您指点一二 带你读懂Spring 事务--认识事务 带你读懂Spring 事务--事务的传播机制(藏) 特别提示:本文所进行的实验都是在MySQL ...

  8. 无线充电技术究竟有何神秘之处?一篇文章带你读懂什么是无线充电技术

    无线充电技术这个概念在很早之前就已经被提出了,发展至今在电子领域已经被深入研究应用,虽然还未曾大范围普及,但在消费电子领域的发展已经取得不错的成绩.手机厂商也纷纷在自家旗舰机上加入这一革新性的先进充电 ...

  9. JVM学习笔记(Ⅰ):Class类文件结构解析(带你读懂Java字节码,这一篇就够了)

    JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码 前言:本文属于博主个人的学习笔记,博主也是小白.如果有不对的地方希望各位帮忙指出.本文主要还是我的学习总结,因为网上的一些知识分 ...

最新文章

  1. SAP QM初阶之明明存在检验计划但是生成的检验批取不到?
  2. Linux监控(添加自定义监控项,配置邮件告警)
  3. popen函数_PHP中16个高危函数
  4. 【LeetCode+51nod】股票低买高卖N题
  5. Unity3d发布web版无法连接数据库
  6. 小白也能看懂的 Java 异常处理
  7. Android.mk的一些FAQ
  8. grub2 中的boot命令
  9. Charles青花瓷安装使用及断点设置
  10. 模拟实现求字符串长度函数strlen
  11. chrome下载文件竟然都有缓存!
  12. 智慧旅游综合运营服务平台建设方案
  13. matlab 积分函数曲线,matlab积分函数
  14. luogu 3374
  15. Java拼图游戏总结,Java拼图游戏课程设计报告
  16. Unity3D操作数据之Txt文档操作(创建、读取、写入、修改)
  17. 移动群智感知应用学习
  18. 软件工程复习——第4章
  19. PTA寒假基础题训练(含解题思路)(下)
  20. 【原创】关于联想Y400适配器和电池同时使用无法开机的问题

热门文章

  1. machine learning ex3
  2. 如何访问部署在家里的服务器(家庭宽带能够申请公网IP)
  3. 【SQL中阶】 SQL多表联查(配合多条件复杂查询)
  4. 想要显年轻?5个习惯只要你坚持,会看到不一样
  5. 【计算机图形学】中点画圆算法和Bresenham画圆算法
  6. 数字化改造转型到底意味着什么
  7. 相对论【2】杆和钟的运动
  8. Apache Beam指南
  9. 差异表达基因提取limma+WGCNA分析全代码
  10. 第四届CECC中国计算机教育大会召开,飞桨持续加码产教融合教育新生态