分分钟带你读懂 ButterKnife 的源码
为什么要写这一系列的博客呢?
因为在 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 的源码相关推荐
- 人人都能读懂的react源码解析(大厂高薪必备)
人人都能读懂的react源码解析(大厂高薪必备) 1.开篇(听说你还在艰难的啃react源码) 本教程目标是打造一门严谨(严格遵循react17核心思想).通俗易懂(提供大量流程图解,结合demo ...
- 分分钟带你读懂-ButterKnife-的源码
} }); target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", ...
- 播放失败246106异常代码_web前端面试题:您能读懂的Promise源码实现(手写代码)...
Promise 是 web 前端工程师在面试的过程中很难绕过的一个坎.如果您目前处于对 Promise 一知半解,或仅仅是停留在可以使用的层面上,建议您跟着本文敲打练习一遍,相信您一定会有所收获!另外 ...
- web前端面试题:你能读懂的Promise源码实现(手写代码)
Promise是web前端工程师在面试的过程中很难绕过的一个坎.如果您目前处于对Promise一知半解,或仅仅是停留在可以使用的层面上,建议您跟着本文敲打练习一遍,相信您一定会有所收获!另外文章有点长 ...
- java 源码系列 - 带你读懂 Reference 和 ReferenceQueue
java 源码系列 - 带你读懂 Reference 和 ReferenceQueue https://blog.csdn.net/gdutxiaoxu/article/details/8073858 ...
- 带你读懂Spring Bean 的生命周期,嘿,就是玩儿~
带你读懂Spring Bean 的生命周期,嘿,就是玩儿~ 一.前言 今天我们来说一说 Spring Bean 的生命周期,小伙伴们应该在面试中经常遇到,这是正常现象.因为 Spring Bean 的 ...
- 带你读懂Spring 事务——事务的隔离级别(超详细,快藏)
不了解事务的铁汁可以先看前两篇,讲的超详细,有问题还请您指点一二 带你读懂Spring 事务--认识事务 带你读懂Spring 事务--事务的传播机制(藏) 特别提示:本文所进行的实验都是在MySQL ...
- 无线充电技术究竟有何神秘之处?一篇文章带你读懂什么是无线充电技术
无线充电技术这个概念在很早之前就已经被提出了,发展至今在电子领域已经被深入研究应用,虽然还未曾大范围普及,但在消费电子领域的发展已经取得不错的成绩.手机厂商也纷纷在自家旗舰机上加入这一革新性的先进充电 ...
- JVM学习笔记(Ⅰ):Class类文件结构解析(带你读懂Java字节码,这一篇就够了)
JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码 前言:本文属于博主个人的学习笔记,博主也是小白.如果有不对的地方希望各位帮忙指出.本文主要还是我的学习总结,因为网上的一些知识分 ...
最新文章
- SAP QM初阶之明明存在检验计划但是生成的检验批取不到?
- Linux监控(添加自定义监控项,配置邮件告警)
- popen函数_PHP中16个高危函数
- 【LeetCode+51nod】股票低买高卖N题
- Unity3d发布web版无法连接数据库
- 小白也能看懂的 Java 异常处理
- Android.mk的一些FAQ
- grub2 中的boot命令
- Charles青花瓷安装使用及断点设置
- 模拟实现求字符串长度函数strlen
- chrome下载文件竟然都有缓存!
- 智慧旅游综合运营服务平台建设方案
- matlab 积分函数曲线,matlab积分函数
- luogu 3374
- Java拼图游戏总结,Java拼图游戏课程设计报告
- Unity3D操作数据之Txt文档操作(创建、读取、写入、修改)
- 移动群智感知应用学习
- 软件工程复习——第4章
- PTA寒假基础题训练(含解题思路)(下)
- 【原创】关于联想Y400适配器和电池同时使用无法开机的问题